首先还是感谢大佬及其伙伴的文章,对我在面试准备期间帮助巨大:面试官:Javascript如何实现继承? | web前端面试 - 面试官系列 (vue3js.cn)
本文主要是对其中的代码及核心代码发表自己的理解(非常深刻)然后就是上述的文章中给出的部分示例代码可能有点狂放,有些没定义的可能他们也没注意,看肯定还是能看懂的,下文大部分的解释和例子都是引用上面的链接文章,原文也是极好的!
前情提要:要了解原型以及原型链的相关知识,不然可能会看懵
可以参考:原型和原型链-CSDN博客
核心代码已经在代码块中标出 (≧∇≦)ノ
1.原型链继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child2';
}
Child.prototype = new Parent(); // 核心代码
console.log(Child.prototype); //Parent
var s1 = new Child();
var s2 = new Child();
console.log(s1.__proto__); //Parent
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]
改变s1
的play
属性,会发现s2
也跟着发生变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的
解析:
① Child.prototype = new Parent(); 改变了 Child 的原型链,name 和 play 属性都定义在其原型链上,当 s1.play.push(4) 运行时,会首先寻找 s1 上的属性,发现没有则往其原型链上寻找,即 s1.__proto__ ,会寻找到 Parent ,发现 Parent 有这个属性,于是拿到,继承完成
② 啰嗦一句,可能有人会觉得 new Parent() 不应该是构造了一个全新的 Parent 吗,怎么会使得他们共享内存空间呢,但这个 Parent 只是单纯的新建一个出来,不然没法进行指向。本质上,两个实例对象新建的还是 Child 函数,而 Child 的原型对象就是 Parent ,所以他们指向的还是同一个
2.构造函数继承
借助 call
调用Parent
函数
function Parent(){
this.name = 'parent1';
console.log("我被执行了");
}
Parent.prototype.getName = function () {
return this.name;
}
function Child(){
Parent.call(this); // 核心代码
this.type = 'child'
}
let child = new Child();
let child1 = new Child();
console.log(child); // 没问题
console.log(child.getName()); // 会报错
好处:相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端
坏处:只能继承父类的实例属性和方法,不能继承父类原型属性或者方法
解析:
① 该方法的核心代码在于Parent.call(this); 这句代码我们要拆开来解读,首先是 Parent. 此时其实是 new 了一个新的 Parent 对象来调用 call 方法(请看下面的图片),其次,call 方法就是用来改变 this 指向,所以,新的 Parent 对象的全部的属性以及方法都会转移到新的 Child上
② 重新梳理一下:即,当 let child = new Child(); 执行时,进入function Child(),执行Parent.call(this); 新建了一个 Parent 对象来调用 call 方法,改变 this 指向,使新的 Parent 对象的全部的属性以及方法都会转移到新的 Child上
3.组合继承
前面我们讲到两种继承方式,各有优缺点。组合继承则将前两种方式继承起来
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this);
this.type = 'child3';
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
解析:其实就是把上面两种方式的优点结合起来
用第一种原型链方法时,能完善原型链,使得 getName() 方法能成功被获取到
用第二种构造函数方法时,能使数据之间互不影响
4.原型式继承
这里主要借助Object.create
方法实现普通对象的继承
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function () {
return this.name;
}
};
let person4 = Object.create(parent4); //核心代码
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4); //核心代码
person5.friends.push("lucy");
/// 重点在这
console.log(person4.__proto__ === parent4); // true
console.log(person5.__proto__ === parent4); // true
///
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
解析:
① 首先着重讲一下 Object.create() 方法,其实也是改变原型链,达到继承的效果,在上述代码中,存在 person4.__proto__ === parent4 ,其实可以简单理解为,Object.create() 可以使得左边的变量的 __proto__ 指向右边的变量,这很关键。
举个例子: let a = Object.create(b) ===> a.__proto__ === b
这种继承方式的缺点也很明显,因为Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能
5.寄生式继承
寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original); // 核心代码
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]
解析:其实本质上还是上一种方法,只是懒得一个个加新的函数(万一要加的函数很多呢),我感觉有点像一种代码复用
其优缺点也很明显,跟上面讲的原型式继承一样
6.寄生组合式继承
寄生组合式继承,借助解决普通对象的继承问题的Object.create
方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式
function clone(parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype); // child.prototype.__proto__ = parent.prototype
child.prototype.constructor = child;
}
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
this.identify = {
a: '123'
}
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
let person7 = new Child6();
// 关键在这
console.log(person7.__proto__ === Child6.prototype); //true
console.log(Child6.prototype.__proto__ === Parent6.prototype); // true
//
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person7); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5
解析:有点像大杂烩,其中的原理上面都有提到,我这里就把原型链画出来供大家参考一下