欢迎大家到访我的github: https://github.com/suilfly
我的个人博客:https://suilfly.github.io/
实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的
原型链继承
构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性(constructor)指回构造函数,而实例有一个内部指针([[ prototype ]])指向原型。
/* 构造函数 */
function Father(){
this.fatherName="f1";
}
console.dir(Father);/*构造函数内有prototype: {constructor: ƒ Father()} constructor指向father */
let fatherInstance = new Father();
console.log(fatherInstance.__proto__);/* fatherInstance为Father的实例:实例中有一个指针指向Father原型,输出{constructor: ƒ Father()} */
实现继承
function Father(){
this.fatherName="f1";
}
Father.prototype.f = "pf"
function Son(){
this.sonName="s1";
}
/* 现在想要Son继承Father,已知Son的构造函数的prototype:{constructor f Son()} ,那么只要改变Son的构造函数的原型即可*/
/* Son.prototype = Father.prototype; 不能使用此方法改变Son的原型,因为如此改变后,Son的实例只能使用自己的属性和Father原型上的属性,并没有继承Father实例中的属性,而且以后想要修改Son的原型必定会破坏Father的原型*/
Son.prototype = new Father();
console.log(s instanceof Father);//true
console.log(s instanceof Son);//true
- 现在想要Son继承Father,已知Son的构造函数的prototype:{constructor f Son()} ,那么只要改变Son的构造函数的原型即可
- Son.prototype = Father.prototype; 不能使用此方法改变Son的原型,因为如此改变后,Son的实例只能使用自己的属性和Father原型上的属性,并没有继承Father实例中的属性,而且以后想要修改Son的原型必定会破坏Father的原型
原型与实例的关系
- 方式一
/* 上面用到了instanceof来检测原型与实例的关系 */
console.log(s instanceof Object);//true,找到s原型链上有和Object一样的构造函数
console.log(s instanceof Father);//true
console.log(s instanceof Son);//true
/* instanceof的原理:查找s的原型链,看看是否有和Object|Father|Son原型链上一样的构造函数 */
-
方式二
- 确定这种关系的第二种方式是使用 isPrototypeOf()方法。原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,这个方法就返回 true:
Father.prototype.isPrototypeOf(s);//true ,s的原型链中包含Father原型
Son.prototype.isPrototypeOf(s);//true ,s的原型链中包含Father原型
Object.prototype.isPrototypeOf(s);//true ,s的原型链中包含Father原型
原型链的问题
- 某一实例成为构造函数原型后,共享内存的问题
function Father(){
this.fatherFood=['面条','鸡蛋'];
}
function Son(){
this.sonFoos = ['肯德基']
}
/* 继承 */
Son.prototype = new Father();
let s = new Son();
s.fatherFood.push('鸡腿');
let s2 = new Son();
s2.fatherFood;//["面条", "鸡蛋", "鸡腿"],但是s2的fatherFood也改变
- 原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。就导致原型链基本不会被单独使用,用来修改继承关系。
盗用构造函数继承
function Father(name){
this.fatherFood=['面条','鸡蛋'];
this.name = "ff";
}
function Son(){
this.sonFoos = ['肯德基'];
Father.call(this,"ss");/* 执行Father,并修改this指向Son的实例,可以在子类构造函数中向父类构造函数传参 */
}
let s = new Son();
s.fatherFood.push('鸡腿');
let s2 = new Son();
s2.fatherFood;//["面条", "鸡蛋"],解决了共享内存问题
s instanceof Father;//false
s instanceof Son;//true
- 盗用构造函数的问题:子类也不能访问父类原型上定义的方法,所以基本也不用此方法来修改继承关系
组合继承
组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。
function Father(name){
this.fatherFood=['面条','鸡蛋'];
this.name = "ff";
}
function Son(){
this.sonFoos = ['肯德基'];
/* 继承属性 */
Father.call(this,"ss");
}
Son.prototype = new Father();/* 继承父方法 */
let s = new Son();
s.fatherFood.push('鸡腿');
let s2 = new Son();
s2.fatherFood;//["面条", "鸡蛋"],解决了共享内存问题
- 组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。
寄生式组合继承
- 其实组合继承也有一个问题:效率不是很高,因为它要执行两次父级构造函数,第一次在子级构造函数内,第二次在修改子级原型时
/* 寄生式顾名思义,在一个函数内部做了某些操作 */
function inheritPrototype(Father,Son){
let prototype = Object.create(Father.prototype);/* Object.create(proto,[propertiesObject]);proto 新创建对象的原型对象 */
prototype.constructor = Son;//修改constructor,因为之后要把这个prototype作为Son的原型,那么Son原型中的constructor一定要指向Son啦
Son.prototype = prototype;
}
function Father(name){
this.fatherFood=['面条','鸡蛋'];
this.name = "ff";
}
Father.prototype.ff = function(){ console.log("function of father") }
function Son(){
this.sonFoos = ['肯德基'];
/* 继承属性 */
Father.call(this,"ss");
}
inheritPrototype(Father,Son);
- 这里只调用了一次 Father 构造函数,避免了 Son.prototype 上不必要也用不到的属性,
因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和
isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。