前面的话
许多的oo语言都支持两种继承方式:接口继承与实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。js只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
原型链继承的本质
原型链继承的本质是重写原型对象,代之是一个新类型的实例.
下列代码中,Fn的新原型不仅有new Fn1()实例上的全部属性和方法,并且还继承了Fn1原型上的属性与方法。
function Fn1() {
this.property1 = true;
}
Fn1.prototype.getFn1Vlaue = function () {
return this.property1;
}
function Fn2() {
this.property2 = false;
}
Fn2.prototype = new Fn1();// Fn1 的实例为Fn2的原型
Fn2.prototype.getFn2Value = function () {// Fn2原型上添加方法
return this.property2;
}
var instance = new Fn2();// 创建Fn2的实例
console.log(Fn2.prototype);
console.log(instance.__proto__);
console.log(instance.__proto__.__proto__);
console.log(Fn2.prototype.constructor === Fn1);
console.log(instance.constructor)
console.log(instance.getFn1Vlaue());
instance实例可以访问Fn1中的属性,原因就是因为Fn2继承了Fn1,而继承是通过创建Fn1实例实现的。
Fn2的原型对象包含Fn1实例的所以属性和方法,然后在Fn2的原型对象上又添加了一个getFn2Value方法。
instance是Fn2的实例,所以instance.__proto__与Fn2.prototype返回的对象一样。
instance. __ proto __.__proto__理所当然返回的就是Fn1.prototype。因为Fn2.prototype被重写了为Fn1,所以Fn2.prototype.constructor === Fn1,相应的instance.constructor指向Fn1。
说明:instance.getFn1Vlaue()会经历三个步骤:1) 搜索实例 2)Fn2.prototype 3)Fn1.prototype。在第三步找到这个方法。
instanceof 操作符原理
instanceof操作符用来检测 实例与原型链中出现过的构造函数,结果返回true。
上面的例子
console.log(instance instanceof Fn2);
console.log(instance instanceof Fn1);
console.log(instance instanceof Object);
instanceof原理就是沿着原型链上一层一层查找,只要instance.[proto…] === instance.constructor.prototype,那么instance instanceof constructor就返回true。
还有一种方法是使用原型对象上的isPrototypeOf()方法,原理一样:
console.log( Fn2.prototype.isPrototypeOf(instance));
console.log( Fn1.prototype.isPrototypeOf(instance));
console.log( Object.prototype.isPrototypeOf(instance));
原型链继承的问题
- 多个实例对引用类型的操作会被篡改
- 子类型的原型上的constructor被重写了(上例中的Fn2.prototype.constructor === Fn1)
- 给子类型原型添加属性和方法必须在替换原型之后
- 创建子类型实例时无法向父类型的构造函数传参
[问题 1]
function Fn1() {
this.colors = ['red', 'blue', 'green'];
}
Fn1.prototype.getColors = function () {
console.log(this.colors);
}
function Fn2() {
}
Fn2.prototype = new Fn1();// 继承了Fn1
var instance1 = new Fn2();
instance1.colors.push('black');
instance1.getColors();
var instance2 = new Fn2();
instance2.getColors();
构造函数Fn1中定义了一个colors属性,该属性包含一个数组(引用类型)。将Fn1的实例赋给Fn2.prototype,那么Fn1的实例属性colors就变成了Fn2.prototype的属性,又因为原型属性上的引用类型会被所有实例共享,所以多个实例对引用类型的操作会被篡改。
[问题 2]
子类型原型的上的额constructor属性被重写了,执行了Fn2.prototype = new Fn1()后原型被重写.
console.log(Fn2.prototype.constructor === Fn1);// true
解决方法就是重写Fn2.prototype.constructor属性,指向自己的构造函数Fn2
Fn2.prototype = new Fn1();
Fn2.prototype.constructor = Fn2;
[问题 3]
给子类型原型添加属性和方法必须在替换原型之后,原因就是因为在Fn2.prototype = new Fn1()之后Fn2.prototype会被覆盖
function Fn1() {
this.colors = ['red', 'blue', 'green'];
}
Fn1.prototype.getValue= function () {
console.log(this.colors);
}
function Fn2() {
}
Fn2.prototype = new Fn1();// 继承了Fn1
Fn2.prototype.constructor = Fn2;
// 重写了超类型的getValue方法
Fn2.prototype.getValue = function () {
console.log(this.value);
}
var instance = new Fn2();
instance.value = 'xiaoqi';
instance.getValue();
var instance2 = new Fn1();
instance2.getValue();
前面说过instance.getValue()会经过三个步骤:1) 搜索实例 2)搜索Fn2.prototype 3)搜索Fn1.prototype,所以结果为“xiaoqi”。但是如果想用Fn1.prototype上的getValue方法,使用Fn1的实例调用getValue方法就可以了。
[注意]: 在Fn2.prototype = new Fn1()之后,想为Fn2.prototype添加新的属性或方法,不能使用对象字面量的方式 添加,这样会重写原型链。
function Fn1() {
this.colors = ['red', 'blue', 'green'];
}
Fn1.prototype.getColors= function () {
console.log(this.colors);
}
function Fn2() {
}
Fn2.prototype = new Fn1();// 继承了Fn1
// 使用对象字面量添加新的方法,会导致上面的代码无效。
Fn2.prototype = {
getValue() {
console.log(this.value)
}
}
var instance = new Fn2();
instance.getColors();
上面的代码刚刚把Fn1的实例覆给Fn2.prototype,紧接着又将原型替换为一个对象字面量,导致原型被重写, Fn1 与 Fn2之间没有了关系。