[译]JavaScript原型继承原理

766 阅读4分钟

众所周知,JavaScript有原型继承性。但通常情况下,原型继承是通过new操作符实现的,大部分的解释都让人困惑,这篇文章旨在澄清什么是原型继承以及如何使用。

原型继承定义

JavaScript原型通常是这样定义的:

When accessing the properties of an object, JavaScript will traverse the prototype chain upwards until it finds a property with the requested name. Javascript Garden

JavaScript通过__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方法。 为了创建一个新的point对象,我们只需要创建一个对象,并将新的对象的__proto__属性指向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(); // 10 20

这跟上面写的代码完全不同,这里的Point是一个函数,他使用了一个prototype的属性,还有new操作符,这是个什么鬼?

new 到底做了什么

Brendan Eich 在创造JavaScript 时,想让它看起来像传统的面向对象语言,类似于Java、C++。在这些语言中,使用了 new 操作符来创建一个类的实例。所以,他也写了一个JavaScriptnew操作符:

  • C++有构造函数的概念,用来初始化对象实例的属性,因此 new 操作符的对象应该是一个函数。
  • 我们需要把对象的方法放在某个地方,由于我们正在使用原型语言,因此将其放在函数的 prototype 属性中。

new 操作符接收一个函数F和参数arguments: new F(arguments...),总共做了三件事:

  1. 创建类的实例。它是一个空对象,其__proto__属性设置为F.prototype
  2. 实例初始化。带上参数,调用这个方法F,并把this绑定到实例上。
  3. 返回实例。
     function New (f) {
/*1*/  var n = { '__proto__': f.prototype };
       return function () {
/*2*/    f.apply(n, arguments);
/*3*/    return n;
       };
     }

写一个测试用例来检验一下:

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, 20);
p1.print(); // 10 20
console.log(p1 instanceof Point); // true
 
var p2 = New (Point)(10, 20);
p2.print(); // 10 20
console.log(p2 instanceof Point); // true

JavaScript中真正的原型继承

JavaScript规范只给我们提供了 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例子可以改写成:

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

结论

我们已经了解了什么是原型继承,以及实现原型继承的几种方式。 但是,使用__proto__Object.create()有一些缺点:

  • 非标准化__proto__是非标准化的属性,甚至会被废弃。原生的Object.create()方法实现方式和 Douglas Crockford 实现方式并不完全相同。
  • 性能不高:原生的或自定义的Object.create()方法跟new 不同,还没有被优化过。速度会慢上10倍。

引申阅读:

彩蛋

如果你能看懂下面这张图(来自 ECMAScript 标准),你将会有意想不到的收获!

Object/Prototype Relationships