Javascript中的原型继承的实现

701 阅读4分钟

这篇文章是翻译一篇文章《blog.vjeux.com/2011/javasc…

这是法国前端工程师Vjeux 2011年写的一篇文章,用假设、代码表现的方法,通俗易懂的解释了一下Javascript的设计原理,给人醍醐灌顶的感觉。希望你能从中能学到知识,如果内容有误,还请指出,多谢!


翻译:

      上网一搜,遍地都是写Javascript的原型继承这一概念的文章。但,Javascript实际上只是提供了一种方式,使得Javascript表现得像是其它语言中的原型继承——这个方式就是装饰符 new  。因此,网上大部分的解释很难理解,甚至更让你困惑。这篇文章就是解释了Javascript中的原型继承,和它到底是什么使用的。

原型继承定义

当你读到关于Javascript原型继承的文章时,时常看到它的解释:

当访问一个对象的属性时,Javascript会一直顺着原型链向上查找,直到找到这个属性名的值,
或者在链的顶层都没找到,即为undefined。

一般情况下,我们会使用__proto__属性来表示原型链中上一层的对象。我们来看看__proto__和prototype的不同。

注:__proto__不是一个正式、标准的属性,不应该出现在你的代码里。
    这篇文章里,它只是用来解释Javascript中原型继承怎么实现的。

下面代码说明了Javascript引擎如何查找一个属性的(只是为了通俗易懂的解释,不代表Javascript中是这么写的)。

function getProperty(obj, prop) {
    if(obj.hasOwnProperty(prop)) {
        return obj[prop];
    else if(obj.__proto__ !== null)
        return getProperty(obj.__proto__, prop);
    else 
        return undefined
}

我们来看个例子:一个抽象类 —— 坐标点 Point,它有两个坐标值x,y和一个方法print

按照上面的方法 getProperty,我们来实现下原型继承。我们可以定义一个对象Point,有三个属性:x,y,print。要想生成一个新的point,我们只需要将新的Point的__proto__设置成为Point就可以了。(创建了一个原型链,Point为原型链上的上层)

var Point = {
    x:0,
    y:0,
    print: function() {console.log(this.x,this.y);}
};
var p = {
    x: 10,
    y: 20,
    __proto__: Point
}
p.print();  //10,20

Javascript中诡异的继承

奇怪的是,在上面那个定义下写出来的代码是不成立的。实际使用的方式时下面这种:

function Point(x,y) {
    this.x = x;
    this.y = y;
}
Point.prototype = {
    print: function() {
        console.log(this.x,this.y);
    }
};

var p = new Point(10,20);
p.print();

这个上面的代码完全不同。Point现在是一个函数,我们用了prototype属性,还用了new装饰符。怎么回事?

new是怎么工作的?

Brendan Eich设计Javascript时,参考了当时流行的面向对象语言C++、Java,借鉴了继承的定义方式 new: :new来生成一个类的实例。

  • C++有构造函数的概念,用来初始化实例的属性。因此,new装饰符必须指向一个函数。
  • 我们需要找个地方放置公共方法、公共属性。由于我们正在使用的是一个继承性的语言,那么就把它放在函数的一个属性里,名字是prototype。

new装饰符后面跟着一个函数F,参数arguments:new F(arguments...)。它只做了简单的三步:

  1. 生成类的实例。就是一个只有一个属性__proto__的对象。__proto__的值为F.prototype
  2. 初始化实例。函数F被调用,且 this 指向该实例。
  3. 返回这个实例。

现在,我们知道了new做了什么,最后生成了什么,我们用Javascript来演示一下new方法。

function New(f) {
    var n = {__proto__: f.prototype};  //1
    return function() {
        f.apply(n, arguments);         //2
        return n;                      //3
    }
}

//这里可能不好理解,实际上作者是这么设置的,new和函数名称先执行,返回一个函数后,执行这个函数,即:
var f = (new F)(10,10);先执行前一个函数,然后再执行后面的入参。

再举一个小例子,帮助大家理解

function Point(x,y) {
    this.x = x;
    this.y = y;
}
Point.prototype = {
    print: function() {console.log(this.x,this.y);}
}

var p1 = new Point(10,10);
p1.print(); //10,10
console.log(p1 instanceof Point);

var p2 = new Point(20,20);
p2.print(); //20,20
console.log(p2 instanceof Point);

Javascript中真正的原型继承

Javscript规范中,只提供了new装饰符的使用方式。然后,Douglas Crockford使用了一种方式,让new去做真正的原型继承的工作。那就是重写Object.create()。

Object.create = function(parent) {
    function F() {}
    F.prototype = parent;
    return new F();
}

看起来比较奇怪,实际上它做的工作很简单。就只是创建了一个新对象,它的prototype可以设置为任意值。如果可以使用__proto__的话,它可以更加简略:

Object.create = function(parent){
    return { __proto__: parent };
}

还是上面Point的例子,我们用Object.create来实现:

var Point = {
    x:0,
    y:0,
    print: function() {console.log(this.x,this.y);}
};
var p = Object.create(Point);
p.x = 10;
p.y = 20;
p.print(); //10 20

总结

上面已经讲清楚了什么是原型继承,Javascript是怎样通过一种特定的方式完成原型继承的。

然而,真正实现继承的方式(Object.create和__proto__)有些缺点:

  • 规范不允许:__proto__不是标准的属性,甚至是被反对使用的。原生的Object.create()和douglas Crockford的使用也是不完全相同的。
  • 不是最优化方案:Object.create(原生抑或自定义的),远没有使用new的性能好。甚至能慢到10倍以上。