JavaScript 是個沒有實做 class 關鍵字的動態語言,所以會對那些基於類別(class-based)語言(如 Java 或 C++)背景出身的開發者來說會有點困惑。(在 ES2015 有提供 class 關鍵字,但那只是個語法糖,JavaScript 仍然是基於原型(prototype-based)的語言)。

講到繼承,JavaScript 就只有一個建構子:物件。每個物件都有一個連著其他原型(prototype)的私有屬性(private property)物件。原型物件也有著自己的原型,於是原型物件就這樣鏈結,直到撞見 null 為止:null 在定義裡沒有原型、也是原型鏈(prototype chain)的最後一個鏈結。

幾乎所有 JavaScript 的物件,都是在原型鏈最頂端的 Object 實例。

雖然這常被認為是 JavaScript 的一個缺陷,但原型繼承模型實際上,比傳統的 classic 模型更強大。舉例來說,使用原型繼承模型建構一個 classic 模型是相當容易的。

使用原型鍊繼承

繼承屬性

JavaScript 物件是一「包」動態的屬性(也就是它自己的屬性)'並擁有一個原型物件的鏈結,當物件試圖存取一個物件的屬性時,其不僅會尋找該物件,也會尋找該物件的原型、原型的原型……直到找到相符合的屬性,或是到達原型鏈的尾端。

遵照 ECMAScript 標準的 someObject.[[Prototype]] 標記,用於指派 someObject 的原型。從 ECMAScript 2015 開始, [[Prototype]] 使用 Object.getPrototypeOf()Object.setPrototypeOf() 這兩個訪問器(accessors)訪問,等同於非標準,但各大瀏覽器已實做的 __proto__ 屬性。

不要把 someObject.[[Prototype]] 與函式屬性 func.prototype 混淆了。它在函式被用作建構子的時候,指定 [[Prototype]] 要分派到所有由給定函式建立的物件實例(instance)。Object.prototype 屬性代表了原型屬性 Object

以下是嘗試存取屬性時會發生的事:

// 假設我們有個物件 o,它有自己的屬性 a 和 b: {a: 1, b: 2}
// o.[[Prototype]] 有 b 與 c :的屬性 {b: 3, c: 4}
// 最後 o.[[Prototype]].[[Prototype]] 成了 null.
// 這是原型鏈的結末,因為 null 按照定義並沒有 [[Prototype]]。
// 因此,整個原型鏈看起來就像:
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> null

console.log(o.a); // 1
// o 有稱作「a」的屬性嗎?有,該數值為 1。

console.log(o.b); // 2
// o 有稱作「b」的屬性嗎?有,該數值為 2。
// 這個原型還有屬性「b」,但這裡沒有被訪問到。
// 這稱作「property shadowing」。

console.log(o.c); // 4
// Is there a 'c' own property on o? No, check its prototype.
// Is there a 'c' own property on o.[[Prototype]]? Yes, its value is 4.

console.log(o.d); // undefined
// Is there a 'd' own property on o? No, check its prototype.
// Is there a 'd' own property on o.[[Prototype]]? No, check its prototype.
// o.[[Prototype]].[[Prototype]] is null, stop searching,
// no property found, return undefined.

Setting a property to an object creates an own property. The only exception to the getting and setting behavior rules is when there is an inherited property with a getter or a setter.

繼承方法

Javascript 並沒有其他基於類別語言那般定義的方法。在 Javascript 裡,任何函式都能以屬性的方式加到物件中。一個被繼承的函式的行為就像是其他屬性一樣,其中也包含了上述的 property shadowing(在這種情況下叫做 method overriding)。

當繼承函式執行時,this 值指向繼承的物件,而不是在函式內擁有屬性的原型物件。

var o = {
  a: 2,
  m: function() {
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// When calling o.m in this case, 'this' refers to o

var p = Object.create(o);
// p is an object that inherits from o

p.a = 4; // creates an own property 'a' on p
console.log(p.m()); // 5
// when p.m is called, 'this' refers to p.
// So when p inherits the function m of o, 
// 'this.a' means p.a, the own property 'a' of p

產生物件和原型鏈的幾種方法

含有語法結構的物件

var o = {a: 1};

// 新建的 o 有個自己的 [[Prototype]] 稱為 Object.prototype
// o 自己並沒有稱為「hasOwnProperty」的屬性
// 而 hasOwnProperty 是 Object.prototype 的自有屬性。
// 因此 o 從 Object.prototype 繼承了 hasOwnProperty
// Object.prototype 作為其原型多了個 null
// o ---> Object.prototype ---> null

var a = ['yo', 'whadup', '?'];

// 從 Array.prototype 繼承的陣列,含有諸如 indexOf、forEach……之類的方法
// 原型鏈看起來就像:
// a ---> Array.prototype ---> Object.prototype ---> null

function f() {
  return 2;
}

// 從 Function.prototype 繼承的函式,含有諸如 call、bind……之類的方法
// f ---> Function.prototype ---> Object.prototype ---> null

透過建構子

JavaScript 建構子,就、只、是、個、被 new 操作符呼叫的函式。

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v) {
    this.vertices.push(v);
  }
};

var g = new Graph();
// g 是個有著「vertices」與「edges」屬性的物件。
// 在執行 new Graph() 時 g.[[Prototype]] 是 Graph.prototype 的值。

Object.create

ECMAScript 5 引入了新方法:Object.create()。呼叫這個方法就可以建立新的物件。這個物件的原型,為函式的第一個參數。

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (inherited)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); 
// undefined, because d doesn't inherit from Object.prototype

With the class keyword

ECMAScript 2015 introduced a new set of keywords implementing classes. Although these constructs look like those familiar to developers of class-based languages, they are not the same. JavaScript remains prototype-based. The new keywords include class, constructor, static, extends, and super.

'use strict';

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

Performance

The lookup time for properties that are high up on the prototype chain can have a negative impact on performance, and this may be significant in code where performance is critical. Additionally, trying to access nonexistent properties will always traverse the full prototype chain.

Also, when iterating over the properties of an object, every enumerable property that is on the prototype chain will be enumerated.

To check whether an object has a property defined on itself and not somewhere on its prototype chain, it is necessary to use the hasOwnProperty method which all objects inherit from Object.prototype.

hasOwnProperty is the only thing in JavaScript which deals with properties and does not traverse the prototype chain.

Note: It is not enough to check whether a property is undefined. The property might very well exist, but its value just happens to be set to undefined.

Bad practice: Extension of native prototypes

One mis-feature that is often used is to extend Object.prototype or one of the other built-in prototypes.

This technique is called monkey patching and breaks encapsulation. While used by popular frameworks such as Prototype.js, there is still no good reason for cluttering built-in types with additional non-standard functionality.

The only good reason for extending a built-in prototype is to backport the features of newer JavaScript engines, like Array.forEach.

示例

B 要繼承自 A

function A(a) {
  this.varA = a;
}

// What is the purpose of including varA in the prototype when A.prototype.varA will always be shadowed by
// this.varA, given the definition of function A above?
A.prototype = {
  varA: null,  // Shouldn't we strike varA from the prototype as doing nothing?
      // perhaps intended as an optimization to allocate space in hidden classes?
      // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instance variables
      // would be valid if varA wasn't being initialized uniquely for each instance
  doSomething: function() {
    // ...
  }
};

function B(a, b) {
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB: {
    value: null, 
    enumerable: true, 
    configurable: true, 
    writable: true 
  },
  doSomething: { 
    value: function() { // override
      A.prototype.doSomething.apply(this, arguments); // call super
      // ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();

重點是:

  • 型別被定義在 .prototype
  • 你用了 Object.create() 繼承。

prototype and Object.getPrototypeOf

JavaScript is a bit confusing for developers coming from Java or C++, as it's all dynamic, all runtime, and it has no classes at all. It's all just instances (objects). Even the "classes" we simulate are just a function object.

You probably already noticed that our function A has a special property called prototype. This special property works with the JavaScript new operator. The reference to the prototype object is copied to the internal [[Prototype]] property of the new instance. For example, when you do var a1 = new A(), JavaScript (after creating the object in memory and before running function A() with this defined to it) sets a1.[[Prototype]] = A.prototype. When you then access properties of the instance, JavaScript first checks whether they exist on that object directly, and if not, it looks in [[Prototype]]. This means that all the stuff you define in prototype is effectively shared by all instances, and you can even later change parts of prototype and have the changes appear in all existing instances, if you wanted to.

If, in the example above, you do var a1 = new A(); var a2 = new A(); then a1.doSomething would actually refer to Object.getPrototypeOf(a1).doSomething, which is the same as the A.prototype.doSomething you defined, i.e. Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething.

In short, prototype is for types, while Object.getPrototypeOf() is the same for instances.

[[Prototype]] is looked at recursively, i.e. a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething etc., until it's found or Object.getPrototypeOf returns null.

So, when you call

var o = new Foo();

JavaScript actually just does

var o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);

(or something like that) and when you later do

o.someProp;

it checks whether o has a property someProp. If not, it checks Object.getPrototypeOf(o).someProp, and if that doesn't exist it checks Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp, and so on.

結論

在撰寫複雜的可用程式碼之前,理解原型繼承模型很重要。另外,請注意程式碼內原型鏈的長度、必要時打破它們,以避免潛在的效能問題。再來,除非要處理 JavaScript 新語法的相容性,否則原生原型絕對不能被擴展。

文件標籤與貢獻者

 此頁面的貢獻者: iigmir, WellyHong, hiiamyes, Snailpool
 最近更新: iigmir,