原型链
原型链比作用域链要好理解的多。
JavaScript中的每个对象,都有一个内置的_proto_
属性。这个属性是编程不可见的(虽然ES6标准中开放了这个属性,然而浏览器对这个属性的可见性的支持不同),它实际上是对另一个对象或者null
的引用。
当一个对象需要引用一个属性时,JavaScript引擎首先会从这个对象自身的属性表中寻找这个属性标识,如果找到则进行相应读写操作,若没有在自身的属性表中找到,则在_proto_
属性引用的对象的属性表中查找,如此往复,直到找到这个属性或者_proto_
属性指向null
为止。
这个_proto_
的引用链,被称作原型链。
<!--more-->
注意,此处有一个性能优化的问题:往原型链越深处搜索,耗费的时间越多。
原型链和构造函数
JavaScript是一种面向对象的语言,并且可以进行原型继承。
JavaScript中的函数有一个属性prototype
,这个prototype
属性是一个对象,它的一个属性constructor
引用该函数自身。即:
func.prototype.constructor === func; // ==> true
这个属性有什么用呢?我们知道一个,一个函数使用new
操作符调用时,会作为构造函数返回一个新的对象。这个对象的_proto_
属性引用其构造函数的prototype
属性。
因此这个就不难理解了:
var obj = new Func();
obj.constructor == Func; // ==> true
还有这个:
obj instanceof Func; // ==> true
也是通过查找原型链上的constructor
属性实现的。
构造函数生成的不同实例的_proto_
属性是对同一个prototype
对象的引用。所以修改prototype
对象会影响所有的实例。
“子类”继承实现的几种方式
之所以子类要加引号,是因为这里说“类”的概念是不严谨的。JavaScript是一门面向对象的语言,但是它跟Java等语言不同,在ES6标准出炉之前,它是没有类的定义的。
但是熟悉Java等语言的程序员,也希望使用JavaScript时,跟使用Java相似,通过类生成实例,通过子类复用代码。那么在ES6之前,怎么做到像如下代码一样使用类似"类"的方式呢?
var parent = new Parent("Sam");
var child = new Children("Samson");
parent.say(); // ==> "Hello, Sam!"
child.say(); // ==> "Hello, Samson! hoo~~"
child instanceof Parent; // ==> true
我们看到,这里我们把构造函数当做类来用。
以下我们讨论一下实现的几种方式:
最简单的方式
结合原型链的概念,我们很容易就能写出这样的代码:
function Parent(name){
this.name = name;
}
Parent.prototype.say = function(){
console.log("Hello, " + this.name + "!");
}
function Children(name){
this.name = name;
}
Children.prototype = new Parent();
Children.prototype.say = function(){
console.log("Hello, " + this.name + "! hoo~~");
}
这个方式缺点很明显:作为子类的构造函数需要依赖一个父类的对象。这个对象中的属性name
根本毫无用处。
第一次改进
// ...
Children.prototype = Parent.prototype;
// ...
这样就不会产生无用的父类属性了。
然而,这样的话子类和父类的原型就引用了同一个对象,修改子类的prototype
也会影响父类的原型。
这时候我们发现:
parent.say(); // ==> "Hello,Sam! hoo~~"
这第一次改进还不如不改。
第二次改进——临时构造函数/Object.create
function F(){ // empty
}
F.prototype = Parent.prototype;
Children.prototype = new F();
// ...
parent.say(); // ==> "Hello, Sam!"
child.say(); // ==> "Hello, Samson! hoo~~"
这样一来,修改子类的原型只是修改了F
的一个实例的属性,并没有改变Parent.prototype
,从而解决了上面的问题。
在ES5的时代,我们还可以直接这样:
Children.prototype = Object.create(Parent.prototype);
这里的思路是一样的,都是让子类的prototype
不直接引用父类prototype
。目前的现代浏览器几乎已经添加了对这个方法的支持。(但我们下面会仍以临时构造函数为基础)
但是细细思考,这个方案仍有需要优化的地方。例如:如何让父类的构造函数逻辑直接运用到子类中,而不是再重新写一遍一样的?这个例子中只有一个name
属性的初始化,那么假设有很多属性且逻辑一样的话,岂不是没有做到代码重用?
第三次改进——构造函数方法借用
使用apply
,实现“方法重用”的思想。
function Children(name){
Parent.apply(this, arguments);
// do other initial things
}
“圣杯”模式
现在完整的代码如下:
function Parent(name){
this.name = name;
}
Parent.prototype.say = function(){
console.log("Hello, " + this.name + "!");
}
function Children(name){
Parent.apply(this, arguments);
// do other initial things
}
function F(){ // empty
}
F.prototype = Parent.prototype;
Child.prototype = new F();
Children.prototype.say = function(){
console.log("Hello, " + this.name + "! hoo~~");
}
这就是所谓“圣杯”模式,听着很高大上吧?
以上就是ES3的时代,我们用来实现原型继承的一个近似最佳实践。
“圣杯”模式的问题
“圣杯”模式依然存在一个问题:虽然父类和子类实例的继承的prototype
对象不是同一个实例,但是这两个prototype
对象上面的属性引用了同样的对象。
假设我们有:
Parent.prototype.a = { x: 1};
// ...
那么即使是“圣杯”模式下,依然会有这样的问题:
parent.x // ==> 1
child.x // ==> 1
child.x = 2;
parent.x // ==>2
问题在于,JavaScript的拷贝不是 深拷贝(deepclone)
要解决这个问题,我们可以利用属性递归遍历,自己实现一个深拷贝的方法。这个方法在这里我就不写了。
ES6来了
ES6极大的支持了工程化,它的标准让浏览器内部实现类和类的继承:
class Parent {
constructor(name) { //构造函数
this.name = name;
}
say() {
console.log("Hello, " + this.name + "!");
}
}
class Children extends Parent {
constructor(name) { //构造函数
super(name); //调用父类构造函数
// ...
}
say() {
console.log("Hello, " + this.name + "! hoo~~");
}
}
现在浏览器对其支持程度还不高。但是这种写法的确省力不少。让我们对未来拭目以待。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。