原型链比作用域链要好理解的多。
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~~"
这第一次改进还不如不改。
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极大的支持了工程化,它的标准让浏览器内部实现类和类的继承:
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~~"); } }
现在浏览器对其支持程度还不高。但是这种写法的确省力不少。让我们对未来拭目以待。