ECMAScript
中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针。而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。
根据上面的描述,我们可以写出这样一段代码,看这样是否就是实现了继承。
function Apple(name) {
this.name = name;
}
Apple.prototype.sayName = function () {
console.log(this.name);
};
function Banana(name) {
this.name = name;
}
Banana.prototype = new Apple('苹果');
Banana.prototype.constructor = Banana;
function Cherry(name) {
this.name = name;
}
Cherry.prototype = new Banana('香蕉');
Cherry.prototype.constructor = Cherry;
var ch = new Cherry('樱桃');
console.log(ch);
查看在Chrome
下,输出的ch
是什么?其继承状态又如何。
通过查看ch
在Chrome
下的输出,可以看到,结果就是的确实现了继承。
原型链的问题
原型链虽然很强大,可以用它来实现继承,但是它也存在一些问题。其中,最主要的问题来着包含引用类型值的原型。前面介绍过包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要再构造函数中,而不是原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的(子类型对象的)原型属性了。
通过在上面的原型继承的代码中添加几行代码即可看出问题:
function Apple(name) {
this.name = name;
this.friends = ['cats'];
}
Apple.prototype.sayName = function () {
console.log(this.name);
};
function Banana(name) {
this.name = name;
}
Banana.prototype = new Apple('苹果');
Banana.prototype.constructor = Banana;
function Cherry(name) {
this.name = name;
}
Cherry.prototype = new Banana('香蕉');
Cherry.prototype.constructor = Cherry;
var ch = new Cherry('樱桃');
console.log(ch);
var c2 = new Cherry('桃子?');
ch.friends.push('dogs');
console.log(c2);
console.log("ch:" + ch.friends + " #### c2:" + c2.friends);
最后一行的输出为:
ch:cats,dogs #### c2:cats,dogs
。也就是说,改变了
Cherry
的某一个实例的属性,会导致该类型的全部实例的这个属性都会被改变。这并不是我们想要的效果,但是符合原型的逻辑。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上原型中属性共享问题,实践中很少单独使用原型链。
3.2 借用构造函数
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing
)的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()
和call()
方法也可以在(将来)新创建的对象上执行构造函数,如下所示:
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
SuperType.call(this); // 这里的 this 是什么? 当然是 SubType 类型的对象
// todo:注意,这里并没有把 SuperType()当成构造函数调用,而是当成普通函数调用了。
}
var instance = new SubType();
instance.colors.push('black');
console.log(instance.colors); // [ 'red', 'blue', 'green', 'black' ]
var instance2 = new SubType();
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]
上述代码其实等效于下面的写法:
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
// SuperType.call(this); // 这里的 this 是什么? 当然是 SubType 类型的对象
// 另外这句代码到底执行了什么呢? --> 相当于如下的代码:
}
var instance = new SubType();
instance.SuperType = SuperType;
instance.SuperType();
instance.colors.push('black');
console.log(instance.colors); // [ 'red', 'blue', 'green', 'black' ]
var instance2 = new SubType();
instance2.SuperType = SuperType;
instance2.SuperType();
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]
通过输出可以看出,以上两种写法的效果是相同的。
而且,这实际上并不是什么继承。从console.log(instance);
在Chrome
控制台输出可以明显看出这一点:
SubType {colors: Array(4)}
colors
:
(4) ["red", "blue", "green", "black"]
__proto__
:
Object
通过
chrome
查看会更直接(建议把以上任意一种代码方到chrome
下运行。)。
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // false
以上两句代码也可以明确这一点!
个人以为,借用构造函数模式,仅仅是给每个实例对象创建了不共享了实例属性。并且这种方式并没有实现原型继承。
3.3 组合继承
组合继承(combination inheritance
),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者纸厂的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性,如下:
// 第一段
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 第二段
SuperType.prototype.sayName = function () {
console.log(this.name);
};
// 第三段
function SubType(name, age) {
SuperType.call(this, name); // --> this.colors = ['red','blue','green']
this.age = age;
}
// 第四端
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
// 第五段
SubType.prototype.sayAge = function () {
console.log(this.age);
// 这里的 this 是谁?
// 现在还看不出来,要看调用者,但是可以猜测,这个方法的调用者一定是一个 SubType 类型的对象,
// 所以,这里的 this 就是一个 一个 SubType 类型的对象
};
// 第六段
var s1 = new SubType('Tom', 29);
s1.colors.push('black');
console.log(s1.colors);
s1.sayName();
s1.sayAge();
var s2 = new SubType('Ann', 33);
console.log(s2.colors);
s2.sayName();
s2.sayAge();
上述代码就解决了原型对象上面定义的属性会被实例共享的尴尬(此尴尬参见:3.1 原型链)。
-<>- 先来分析一下上面的代码,为什么这样就 既实现了函数复用,又能保证每个实例都有它自己的属性。
前面的两段代码不用分析,就是典型的组合使用构造函数和原型模式的代码。然后是第三段代码:
// 3
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
注意这里的SuperType.call(this, name);
,千万不要被SuperType
是构造函数的概念给唬住了。(每个构造函数都可以是普通函数。只要不是通过new
操作符去调用的函数,都是普通函数。)在这句代码里,使用了call()
语法。call(thisValue,args)
,因为是call()
,也就是相当于SubType
的实例有一个普通函数叫做SuperType
,然后在此时调用了。而这一调用,就是给自己添加了两个属性name
和colors
。
然后后面一句this.age = age;
就不用说了。
然后是第4段代码:
// 4
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
这段代码,在原型链中已经见识过,就是将当前构造函数的原型重写为父类型的实例。(并且重写自己构造函数。)通过修改原型,实现了继承。
然后是第5段代码:
// 5
SubType.prototype.sayAge = function () {
console.log(this.age);
};
这段代码也很简单,就是通过自己的原型属性,添加一个共享方法。
然后第6段代码是验证性代码,也不必解释了。
通过对上述代码的分析可知:实现函数复用的代码段是[第4段代码],保证每个实例都有它自己的属性的代码是[第3段代码]。
在这个例子中,SuperType
构造函数定义了两个属性:name
和colors
。SubperType
的原型定义了一个方法sayName()
。SubType
构造函数在调用SubperType
构造函数时传入了name
参数。(实际上,这里是把SuperType()
当成普通函数去使用的。)紧接着,又定义了它自己的属性age
。然后,将SuperType
的实例赋值给SubType
的原型,然后又再该新原型上定义了方法sayAge()
。这样一来就可以让两个不同的SubType
实例既分别拥有自己的属性(包括colors
属性),又可以使用相同的方法了。
以上的实现都有一些不足之处,不过组合继承可以作为常用方法。相对于组合继承,还有一直更好的方法。叫做寄生组合式继承
function extend(Child, Parent) {
var F = function () {
};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
function Fruit(name) {
this.name = name;
this.coolors = ['red', 'blue', 'green'];
}
Fruit.prototype.sayName = function () {
console.log(this.name);
};
function Banana(name, age) {
Fruit.call(this, name); // 这一句是必须要的哦
this.age = age;
}
extend(Banana, Fruit);
Banana.prototype.sayAge = function () {
console.log(this.age);
};
// 子类型原型方法写在后面是对的,如果父类型的原型中有同名方法,子类型的方法可以屏蔽父类型的。
var banana = new Banana('香蕉', 12);
console.log(banana);
其中的
extend
方法来自阮一峰——Javascript面向对象编程(二):构造函数的继承。
为什么这样写,其实和 组合继承很类似,这样写是为了减少内存占用。详细分析可以参阅链接。
好了,以后继承可以都使用这种寄生组合模式了。