前端面试基础-面向对象
前言
记录前端面向对象的学习 学习已完成
- 1.面向对象
- 2.JS创建对象的几种方式
- 3.JS实现继承的几种方式
- 4.JS实现深克隆
- 5.this指向的几种情况
1.面向对象
面向对象三大特点(封装、继承、多态)
1.封装
定义:创建一个对象,集中保存现实中一个事物的属性和功能。零散的数据封装进对象结构中,便于大量数据的管理维护。
a.封装对象的第一种方式 - 直接量
var obj={ // new Object()的简写
a: 1,
b: 2
fn: function(){
console.log(this.a)
}
}
b.封装对象的第二种方式 - new Object()
var obj= new Object();
obj.a = 1;
obj.fn = function(){
console.log(this.a)
}
obj.a 的访问方式 揭示了JS语言底层最核心的原理:JS中所有对象底层都是关联数组
-
- 存储结构: 都是键值对的组合 \
-
- 访问成员时:
- 标准写法都是: 对象名/数组名["成员名"]
- 简写都是: 对象名/数组名.成员名吗,比如: obj[“a”]和obj.a都行
-
- 强行给不存在的位置赋值不但不会报错,而且还会自动添加该属性。所以给对象添加一个新属性或新方法,都可通过强行赋值的方式。\
-
- 强行访问不存在的位置的值都不报错而是返回undefined
-
- 都可以用 for in 循环遍历
c.封装对象的第三种方式 - 构造函数
function Obj(a) {
this.a = a;
this.b = b;
this.fn = function(){
console.log(this.b)
}
}
var obja = new Obj(1) // 使用构造函数可以反复创建多个对象
new做了4件事
- 1. 创建一个新的空对象
- 2. 让子对象继承构造函数的原型对象(继承看下面)
- 3. 调用构造函数
- 将构造函数中的this -> new 创建的新对象
- 在构造函数内通过强行赋值方式,为新对象添加规定的属性和方法
- 4. 返回新对象的地址,保存到=左边的变量里
总结this指向两种情况:
- obj.fun() fun中的this->.前的obj对象
- new Fun() Fun中的this->new创建的新对象
2.继承
抛出问题:方法定义放在构造函数中,每次new时都会执行function,就会反复创建相同函数的多个副本!浪费内存 解决:所以多个子对象都要使用相同的功能和属性值时,都可以用继承来解决 定义:父对象中的成员,子对象无需重复创建,就可直接使用!就像使用自己的成员一样!this.属性名/方法名()
JS中的继承都是通过原型对象实现的(原型对象:所有子对象集中保存共有属性值和方法的父对象)
构造函数中都有一个自带的属性prototype,指向自己配对的原型对象。构造函数.prototype 如何继承:(此处承接上面new的第二件事) • new的第二步自动让新创建的子对象,继承构造函数的原型对象。 • new的第二步自动设置子对象的__proto___ 属性,指向构造函数的原型对象。
// 添加共有属性
function Obj(a) {
this.a = a;
this.b = b;
this.fn = function(){
console.log(this.b)
}
}
Obj.prototype.fn = function(){} // 直接给构造函数的prototype(即给原形对象直接添加方法强制赋值)
var obja = new Obj(1)
obja.__proto__ == Obj.prototype // true 子对象指向父对象的prototype
原型对象中的this
- 判断this,只看在哪里如何调用!不要看定义在哪
- 因为原型对象中的共有方法将来使用”子对象.方法名()”调用。
- 原型对象中的this->将来调用这个公共函数的**.(点)**前的那个子对象。
- 简单记: 谁调用指谁
function Student(name,age){
this.name = name;
this.age = age;
}
Student.prototype.intro = function(){
console.log(this.name, this.age)
}
var zs = new Student('zs', 18)
var ls = new Student('ls', 19)
zs.intro() // ('zs', 18)
ls.intro() // ('ls', 19)
this指向第三种:原型对象中共有方法的this->将来调用共有方法的 .(点) 前的子对象
这里还可扩展 原型链:由多级父对象逐级继承形成的链式结构,此处暂不多讲,this指向可参考这篇文章 this指向的几种情况
3.多态
定义:同一个函数在不同情况下表现出不同的状态
- a. 重载overload: 同一个函数,输入不同的参数,执行不同的逻辑
- b. 重写overwrite: (推翻、遮挡) 从父对象继承来的成员不好用,都在子对象中重写同名成员
var ww = new Student('ww',20)
ww.intro = function () {
console.log('overwrite', this.name, this.age)
}
ww.intro() // (overwrite, ww, 19)
2.JS创建对象的10种方式总结
上面介绍了三种
1. 直接量 var obj = {}
2. new Object()
3. 构造函数 function Obj(){}
4. 工厂函数方式
// 本质还是Object(),将来无法根据对象的原型对象准确判断对象的类型
function createObj(name,age){
var obj = new Object()
obj.name = name;
obj.age = age;
obj.intro = function(){ console.log(this.name,this.age) }
return obj;
}
console.log('obj: ', createObj('ty',18));
5. 原形对象方式
// 先创建完全相同的对象,再给子对象添加个性化属性,这个步骤比较繁琐,重复在写prototype
function Obj() { }
Obj.prototype.name = 'ty';
Obj.prototype.age = 18;
Obj.prototype.intro = function () { console.log(this.name, this.age) };
var o1 = new Obj();
o1.name = 'cj' // 禁止修改共有属性,这里会自动创建 o1 自由的属性 name
console.log('o1: ', o1);
var o2 = new Obj();
o2.name = 'wd' // 同上
console.log('o2: ', o2);
6. 混合模式(构造函数+原型对象)
// 此方式不符合面向对象封装的思想
function Obj(name, age) {
this.name = name;
this.age = age;
}
Obj.prototype.intro = function () {
console.log(this.name, this.age)
};
var o1 = new Obj('ty', 18);
console.log(o1);
7. 动态混合
// 先创建完全相同的对象,再给子对象添加个性化属性。
// 缺点: 针对第6点不符合封装,改造的动态混合虽然符合封装,但语义不符,其实if只在创建第一个对象时有意义。
function Obj(name, age) {
this.name = name;
this.age = age;
if (Obj.prototype.intro === undefined) {
Obj.prototype.intro = function () {
console.log(this.name, this.age)
};
}
}
var o1 = new Obj('ty', 18);
o1.intro();
8. 寄生构造函数
// 构造函数里调用其他的构造函数,这种方式可读性较差
function Obj(name, age){}
function Obj2(name, age, height) {
var o = new Obj(name, age);
o.height = height;
return o
}
var o1 = new Obj('ty', 18, 180);
console.log('o1: ', o1);
9. 稳妥构造函数:闭包
// 优点:不用this,不用new!安全,可靠
// 缺点:闭包容易造成内存泄漏
function Obj(name, age) {
var o = {
getName: function () { return name },
setName: function (val) { name = val },
getAge: function () { return age }
}
return o
}
var o1 = new Obj('ty', 18);
console.log('o1: ', o1);
简单判断闭包形成
10. ES6 Class
3.JS实现继承的7种方式
function Fruit(name) { // 定义一个父类型构造函数
this.name = name;
this.play = [1,2,3]
}
Fruit.prototype.intro = function (color) { // 原型对象方法
console.log(this.name + ' is ' + color)
}
1. 原型链式继承
将父类的实例作为子类的原型 缺点:
- 此方式创建子类实例时无法向父类构造函数传参
- 这是因为两个实例使用的是同一个原型对象,内存空间是共享的,改变
s1
的play
属性,会发现s2
也跟着发生变化了
function Apple(name){}
Apple.prototype = new Fruit()
Apple.prototype.name = 'apple'; // 只能继续在原型上添加参数
var s1 = new Apple();
var s2 = new Apple();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]
2. 构造函数继承
借助 call
调用Parent
函数,可传参
缺点:可以看到,父类原型对象中一旦存在父类之前自己定义的方法 intro()
,那么子类将无法继承这些方法
相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法
function Apple(name, color) {
Fruit.call(this, name) // 将子的this替换父类的this
this.color = color
}
var apple = new Apple('apple', 'red')
console.log(apple.name); // 没问题
console.log(apple.intro()); // 会报错
3. 实例继承
function Apple(name, color) {
var o = new Fruit(name); // 创建子类型实例
o.color = color
return o;
}
var apple = new Apple('apple', 'red')
4. 拷贝继承
纯拷贝则无法获取父类,不可for in遍历的方法,没有继承关系(感觉不能叫继承)
function Apple(name, color) {
var o = new Fruit(name); // 创建子类型实例
for (const k in o) {
Apple.prototype[k] = o[k] // 一个个拷贝到子类的 prototype 这种就没有继承关系了
}
this.color = color
}
var apple = new Apple('apple', 'red')
5. 组合继承
是原型链继承与构造函数继承的组合
需要 使用 Fruit.call(this, name)
和 Apple.prototype = new Fruit()
执行两次,造成了多构造一次的性能开销,所以有了下面的寄生组合继承
function Apple(name, color) {
Fruit.call(this, name)
this.color = color
}
Apple.prototype = new Fruit();
Apple.prototype.constructor = Apple
var apple = new Apple('apple', 'red')
6. 寄生组合继承
组合继承的改良版本
- 写法1
function Apple(name, color) {
Fruit.call(this, name)
this.color = color
}
(function () {
var AppleNew = function () { } // 创建一个空的构造函数
AppleNew.prototype = Fruit.prototype; // 将实例的 prototype 作为子类的原型
Apple.prototype = new AppleNew()
})();
Apple.prototype.constructor = Apple
var apple = new Apple('apple', 'red')
- 写法2
function Apple(name, color) {
Fruit.call(this, name)
this.color = color
}
Apple.prototype = Object.create(Fruit.prototype) // 更简洁
Apple.prototype.constructor = Apple
var apple = new Apple('apple', 'red')
7. ES6 Class 继承
4.JS实现深克隆
首先实现浅克隆步骤:
-
- 创建一个空对象
-
- 遍历对象中的所有属性
-
- 每遍历一个属性就为新对象添加同名的新属性,属性值也相同
-
- 返回处理好的对象
var obj = { a: 1, b: 2, c: 3 }
console.log('obj: ', obj);
function shallowClone(obj) {
var newObj = {}
for (const key in obj) {
newObj[key] = obj[key]
}
return newObj
}
var newObj = shallowClone(obj)
console.log('newObj: ', newObj);
浅克隆:是只复制对象的第一级属性值。如果对象的第一级属性中又包含引用类型,则只复制地址。 若需要克隆的对象包含引用类型的属性值,克隆后,新旧对象公用一个引用类型的属性值。任意一方修改了引用类型的内容,则另一方也会受到影响。 所以需要进行深克隆 深克隆:着引用类型的属性值一起克隆,使新旧双方都有自己的引用类型,则哪一方修改都再无瓜葛
实现深克隆的3种方式:
- 1.JSON.stringify() + JSON.parse()
var obj = { a: 1, b: 2, c: 3 }
var strObj = JSON.stringify(obj)
var newObj = JSON.parse(strObj) // 复制的新对象
缺点:无法深克隆undefined值和内嵌函数
-
- Object.assign(target, source)
var obj = { a: 1, b: 2, c: 3 }
var newObj = Object.assign({}, obj) // 复制的新对象
缺点:当对象中只有一级属性,没有二级属性的时,此方法为深拷贝。 但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。
-
- 自定义递归克隆
var obj = { a: 1, b: 2, c: 3 }
function deepClone(obj) {
var newObj = {} // 创建一个空对象,准备接新的副本
if (typeof obj === 'object') {
if (obj === null) {
newObj = null;
} else if (Array.isArray(obj)) { // 数组对象
newObj = [];
for (const key in obj) { // 递归克隆数组中的每一项
newObj.push(deepClone(obj[key]))
}
} else if (obj.constructor === RegExp) { // 正则表达式对象,直接赋值
newObj = obj
} else { // 普通对象,直接循环遍历复制对象中的每个属性值
newObj = {}
for (const key in obj) {
newObj[key] = deepClone(obj[key])
}
}
} else {
newObj = obj;
}
return newObj;
}
console.log(deepClone(obj))
最后
以上的方式总结只是自己学习总结,有其他方式欢迎各位大佬评论 渣渣一个,欢迎各路大神多多指正,不求赞,只求监督指正( ̄. ̄) 有关该文章经常被面试问到的可以帮忙留下言,小弟也能补充完善完善一起交流学习,感谢各位大佬(~ ̄▽ ̄)~
转载自:https://juejin.cn/post/7080423223313039368