【譯】繼承與原型鏈(Inheritance and the prototype chain)

前言

原文來自MDN JavaScript主題的高階教程部分,一共5篇。分別涉及繼承與原型、嚴格模式、類型數組、內存管理、併發模型和事件循環。本篇是第一部分,關於繼承和原型。javascript

原文連接請點我

下面是正文部分:java

對於熟悉基於類的編程語言(例如 Java 和 C++)的開發者來講,JavaScript 會讓他們感到困惑,由於 JS 的動態性以及其自己並不提供class的實現(ES2015 中提出的class關鍵字僅僅是語法糖,JS 仍然是基於原型的)git

提到繼承,JavaScript 只有一個結構:對象(objects)。每一個對象都有一個私有屬性,該屬性連接到另外一個對象(稱爲該對象的原型(prototype))。這個原型對象自身也有一個原型,直到一個對象的原型爲null。根據定義,null不存在原型,它表明這條原型鏈的終點。github

在 JavaScript 中,幾乎全部對象都是Object的實例,Object在原型鏈頂端。web

儘管這種困惑常常被認爲是 JavaScript 的缺點,可是這種原型式的繼承模型實際上比一些經典的模型更爲強大。例如,在一個原型式模型的基礎上再構造一個經典模型是很是簡單的。chrome


經過原型鏈繼承

繼承屬性

JavaScript 對象就像一堆屬性的動態「包裹」(這堆屬性稱爲對象自身屬性)(譯者注:原文爲 JavaScript objects are dynamic "bags" of properties (referred to as own properties).)。
JavaScript 對象有一個指向原型對象的連接。當訪問一個對象的屬性時,不只會在該對象上查找,還會在該對象的原型,以及這個原型的原型上查找,直到匹配上這個屬性名或者遍歷完該原型鏈。編程

根據 ECMAScript 標準, someObject.[[Prototype]]用於指定 someObject的原型。從 ECMAScript 2015 開始, [[Prototype]]能夠經過 Object.getPrototypeOf()Object.setPrototypeOf()訪問。這和經過 JavaScript 中的 __proto__訪問是同樣的,儘管這不標準,可是已經被不少瀏覽器所實現。
最好不要和函數的 _func_.prototype屬性混淆。當一個函數被當作構造器(constructor)調用時,會生成一個對象,而函數上的 _func_.prototype屬性引用的對象會做爲生成對象的 [[Prototype]]存在。 Object.prototype就表示了 Object這一函數的 prototype。

下面例子展現了訪問對象屬性的過程:數組

// 讓咱們使用構造函數f建立一個對象o,o上面有屬性a和b:
let f = function () {
  this.a = 1;
  this.b = 2;
};
let o = new f(); // {a: 1, b: 2}

// 在f的prototype對象上添加一些屬性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要對prototype從新賦值好比: f.prototype = {b:3,c:4}; 這會打斷原型鏈
// o.[[Prototype]] 上有屬性b和c
// o.[[Prototype]].[[Prototype]] 就是 Object.prototype
// 最終, o.[[Prototype]].[[Prototype]].[[Prototype]] 爲 null
// 這就是原型鏈的終端, 等於 null,
// 根據定義, null再也不有 [[Prototype]]
// 所以, 整條原型鏈看起來相似:
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null

console.log(o.a); // 1
// o上存在自身屬性'a'嗎?固然,該屬性值爲1

console.log(o.b); // 2
// o上存在自身屬性'b'嗎?固然,該屬性值爲2
// prototype 上也有屬性'b', 可是並不會被訪問到
// 這叫作「屬性覆蓋」

console.log(o.c); // 4
// o上存在自身屬性'c'嗎?不存在, 繼續查找它的原型
// o.[[Prototype]]上存在自身屬性'c'嗎?固然,該屬性值爲4

console.log(o.d); // undefined
// o上存在自身屬性'd'嗎?不存在, 繼續查找它的原型
// o.[[Prototype]]上存在自身屬性'd'嗎?不存在, 繼續查找o.[[Prototype]]的原型
// o.[[Prototype]].[[Prototype]] 爲 Object.prototype, 上面不存在屬性'd', 繼續查找o.[[Prototype]].[[Prototype]]的原型
// o.[[Prototype]].[[Prototype]].[[Prototype]] 爲 null, 中止查找
// 沒找到屬性'd',返回undefined

在線代碼連接瀏覽器

在一個對象上設置屬性稱爲建立了一個」自身屬性「(譯者注:原文爲Setting a property to an object creates an own property.)。惟一會影響屬性 set 和 get 行爲的是當該屬性使用getter 或者 setter定義。安全

繼承「方法」

JavaScript 中並無像在基於類語言中定義的」方法「。在 JavaScript 中,任何函數也是以屬性的形式被添加到對象中,繼承的函數和其餘繼承的屬性同樣,也存在上面提到的」屬性覆蓋」(這裏叫作方法覆蓋(_method overriding_))。

當一個繼承的函數被執行時,函數內的this指向當前繼承的對象,而不必定是將該函數做爲「自身屬性「的對象自己。

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

console.log(o.m()); // 3
// 當調用 o.m 時, 'this' 指向 o

var p = Object.create(o);
// p 是一個繼承o的對象

p.a = 4; // 在p上建立一個'a'屬性
console.log(p.m()); // 5
// 當調用 p.m 時, 'this' 指向 p.
// 因此當 p 從 o 上繼承了方法 m時,
// 'this.a' 等於 p.a

在 JavaScript 中使用原型

讓咱們更詳細地來看看背後的原理。

在 JavaScript 中,正如上面提到,函數也能夠擁有屬性。全部函數都有一個特殊的屬性prototype。請注意下面的代碼是獨立的(能夠安全地假設網頁中除了下面的代碼就沒有其餘代碼了)。爲了更好的學習體驗,很是推薦你打開瀏覽器的控制檯,點擊'console'標籤,複製粘貼如下代碼,點擊 Enter/Return 鍵來執行它。(大多數瀏覽器的開發者工具(Developer Tools)中都包含控制檯。詳情請查看Firefox 開發者工具Chrome 開發者工具,以及Edge 開發者工具

function doSomething() {}
console.log(doSomething.prototype);
//  無論你如何聲明函數,
//  JavaScript中的函數都有一個默認的
//  prototype 屬性
//  (Ps: 這裏有一個意外,箭頭函數上沒有默認的 prototype 屬性)
var doSomething = function () {};
console.log(doSomething.prototype);

能夠在 console 中看到,doSomething()有一個默認的prototype屬性,打印的內容和下面相似:

{
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

若是咱們在doSomething()prototype上添加屬性,以下:

function doSomething() {}
doSomething.prototype.foo = "bar";
console.log(doSomething.prototype);

結果爲:

{
    foo: "bar",
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

如今咱們能夠經過new操做符來基於這個 prototype 對象建立doSomething()的實例。使用new操做符調用函數只須要在調用前加上new前綴。這樣該函數會返回其自身的一個實例對象。接着咱們即可以往該實例對象上添加屬性:

function doSomething() {}
doSomething.prototype.foo = "bar"; // 往prototype上添加屬性'foo'
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // 往實例對象上添加屬性'prop'
console.log(doSomeInstancing);

打印結果和以下相似:

{
    prop: "some value",
    __proto__: {
        foo: "bar",
        constructor: ƒ doSomething(),
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
}

能夠得知,doSomeInstancing__proto__就是doSomething.prototype。可是,這表明什麼呢?放你訪問doSomeInstancing的一個屬性時,瀏覽器會首先查看doSomeInstancing自身是否存在該屬性。

若是不存在,瀏覽器會繼續查找doSomeInstancing__proto__(或者說是 doSomething.prototype)。若是存在,則doSomeInstancing__proto__的這個屬性會被使用。

不然,會繼續查找doSomeInstancing__proto____proto__。默認狀況下,任何函數 prototype 屬性的__proto__屬性就是window.Object.prototype。所以,會在doSomeInstancing__proto____proto__(或者說是doSomething.prototype.__proto__,或者說是Object.prototype)繼續查找對應屬性。

最終,直到全部的__proto__被查找完畢,瀏覽器會斷言該屬性不存在,所以得出結論:該屬性的值爲 undefined。

然咱們在 console 上再添加一些代碼:

function doSomething() {}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop:      " + doSomeInstancing.prop);
console.log("doSomeInstancing.foo:       " + doSomeInstancing.foo);
console.log("doSomething.prop:           " + doSomething.prop);
console.log("doSomething.foo:            " + doSomething.foo);
console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
console.log("doSomething.prototype.foo:  " + doSomething.prototype.foo);

結果以下:

doSomeInstancing.prop:      some value
doSomeInstancing.foo:       bar
doSomething.prop:           undefined
doSomething.foo:            undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo:  bar

使用不一樣的方法建立對象和原型鏈

使用語法結構(字面量)建立對象

var o = { a: 1 };

// 新建立的對象以 Object.prototype 做爲它的 [[Prototype]]
// o 沒有叫作'hasOwnProperty'的自身屬性
// hasOwnProperty 是 Object.prototype 的自身屬性
// 也就是說 o 從Object.prototype 上繼承了 hasOwnProperty
// Object.prototype 的原型爲 null
// o ---> Object.prototype ---> null

var b = ["yo", "whadup", "?"];

// 數組繼承自 Array.prototype
// (Array.prototype 上擁有方法例如 indexOf, forEach 等等)
// 原型鏈以下:
// b ---> Array.prototype ---> Object.prototype ---> null

function f() {
  return 2;
}

// 函數繼承自 Function.prototype
// (Function.prototype 上擁有方法例如 call, bind, 等等)
// f ---> Function.prototype ---> Object.prototype ---> null

使用構造器函數

構造器函數和普通函數的差異就在於其剛好使用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 提出了一個新方法: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 (繼承自 a )

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

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty);
// undefined, 由於 d 並無繼承自 Object.prototype

Object.createnew操做符一塊兒,使用delete操做符

下面的示例使用Object.create建立一個對象,並使用delete操做符來展現原型鏈的變化

var a = { a: 1 };

var b = Object.create(a);

console.log(a.a); // 1
console.log(b.a); // 1
b.a = 5;
console.log(a.a); // 1
console.log(b.a); // 5
delete b.a;
console.log(a.a); // 1
console.log(b.a); // 1(b.a 的值 5 已經被刪除,所以展現其原型鏈上的值)
delete a.a; // 也可使用 'delete b.__proto__.a'
console.log(a.a); // undefined
console.log(b.a); // undefined

若是換成new操做符建立對象,原型鏈更短:

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

var g = new Graph();
console.log(g.vertices); // print [4,4]
g.vertices = 25;
console.log(g.vertices); // print 25
delete g.vertices;
console.log(g.vertices); // print undefined

使用 class 關鍵字

ECMAScript 2015 提出了一系列新的關鍵字用於實現。包括classconstructorstaticextends以及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);

關於性能

若是須要查找的對象屬性位於原型鏈的頂端,查找時間會對性能有影響,尤爲對於對性能要求很高的應用來講,影響會進一步放大。另外,若是是訪問一個不存在的屬性,老是會遍歷整條原型鏈。

此外,當對對象的屬性進行迭代查找時,原型鏈上全部可枚舉的屬性都會被遍歷。爲了檢查哪些屬性是對象的自身屬性而不是來自其原型鏈,頗有必要使用繼承自Object.prototypehasOwnProperty方法。下面來看一個具體的例子,該例子繼續使用上一個圖形的例子:

console.log(g.hasOwnProperty("vertices"));
// true

console.log(g.hasOwnProperty("nope"));
// false

console.log(g.hasOwnProperty("addVertex"));
// false

console.log(g.__proto__.hasOwnProperty("addVertex"));
// true

hasOwnProperty是 JavaScript 中查找對象屬性時惟一不遍歷原型鏈的方法。

注意:僅僅檢查屬性是undefined並不能表明該屬性不存在,也許是由於它的值剛好被設置爲了undefined

很差的實踐:對原生的 prototypes 進行擴展

常常容易犯的一個錯誤是擴展Object.prototype或者是一些其餘內置的 prototype。

這被稱爲是」猴子補丁「,會打破程序的封裝性。儘管在一些出名的框架中也這樣作,例如 Prototype.js,可是仍然沒有理由在內置類型上添加非標準的功能。

擴展內置類型的惟一理由是保證一些早期 JavaScript 引擎的兼容性,例如Array.forEach(譯者注:Array.forEach是在 ECMA-262-5 中提出,部分早期瀏覽器引擎沒有實現該標準,所以須要 polyfill)

繼承原型鏈的方法總結

下面表格展現了四種方法以及它們各自的優缺點。如下例子建立的inst對象徹底一致(所以控制檯打印的結果也同樣),除了它們之間有不一樣的優缺點。

名稱 舉例 優勢 缺點
使用new初始化 <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = new foo; proto.bar_prop = "bar val"; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); </pre> 支持全部瀏覽器(甚至到IE 5.5),同時,運行速度、標準化以及JIT優化性都很是好 問題是,爲了使用該方法函數必須被初始化。在初始化過程當中,構造函數可能會爲每一個建立對象建立一些特有屬性,然而例子中只會構造一次,所以這些特有信息只會生成一次,可能存致使潛在問題。 以外,構造函數初始化時可能會添加冗餘的方法到實例對象上。不過,只要這是你本身的代碼且你明確這是幹什麼的,這些一般來講也不是問題(其實是利大於弊)。
使用Object.create <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = Object.create( foo.prototype ); proto.bar_prop = "bar val"; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); </pre> <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = Object.create( foo.prototype, { bar_prop: { value: "bar val" } } ); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop) </pre> 支持目前全部的現代瀏覽器,包括非IE瀏覽器以及IE9及以上版本瀏覽器。至關於容許一次性設置proto,這樣有利於瀏覽器優化該對象。同時也容許建立沒有原型的對象例如:Object.create(null) 不支持IE8以及如下版本瀏覽器,不過,微軟目前已再也不支持運行這些瀏覽器的操做系統,對大多數應用來講這也不是一個問題。 以外,若是使用第二個參數,則對象的初始化會變慢,這也許會成爲性能瓶頸,由於第二個參數做爲對象描述符屬性,每一個對象的描述符屬性是另外一個對象。當以對象形式處理成千上萬的對象描述符時,可能會嚴重影響運行速度。
使用Object.setPrototypeOf <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = { bar_prop: "bar val" }; Object.setPrototypeOf( proto, foo.prototype ); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); </pre> <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto; proto = Object.setPrototypeOf( { bar_prop: "bar val" }, foo.prototype ); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop) </pre> 支持目前全部的現代瀏覽器,包括非IE瀏覽器以及IE9及以上版本瀏覽器。支持動態的操做對象的原型,甚至能夠爲Object.create(null)建立的對象強制添加一個原型 因爲性能不佳,應該會被棄用。若是你敢在生產環境中使用這樣的語法,JavaScript代碼快速運行幾乎不可能。由於許多瀏覽器優化了原型,舉個例子,在訪問一個對象上的屬性以前,編譯器會提早肯定原型上的屬性在內存中的位置,可是若是使用了Object.setPrototypeOf對原型進行動態更改,這至關於擾亂了優化,甚至會讓編譯器從新編譯並放棄對這部分的優化,僅僅是爲了能讓你這段代碼跑起來。 同時,不支持IE8以及如下版本瀏覽器
使用proto <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = { bar_prop: "bar val", __proto__: foo.prototype }; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); </pre> <pre lang="javascript"> var inst = { __proto__: { bar_prop: "bar val", __proto__: { foo_prop: "foo val", __proto__: Object.prototype } } }; console.log(inst.foo_prop); console.log(inst.bar_prop) </pre> 支持目前幾乎全部的現代瀏覽器,包括非IE瀏覽器以及IE11及以上版本瀏覽器。將proto設置爲非對象的類型不會拋出異常,可是會致使程序運行失敗 嚴重過期並且性能不佳。若是你敢在生產環境中使用這樣的語法,JavaScript代碼快速運行幾乎不可能。由於許多瀏覽器優化了原型,舉個例子,在訪問一個對象上的屬性以前,編譯器會提早肯定原型上的屬性在內存中的位置,可是若是使用了proto對原型進行動態更改,這至關於擾亂了優化,甚至會讓編譯器從新編譯並放棄對這部分的優化,僅僅是爲了能讓你這段代碼跑起來。 同時,不支持IE10及如下版本瀏覽器。

prototypeObject.getPrototypeOf

對於從 Java 和 C++過來的開發者來講,JavaScript 會讓他們感到有些困惑,由於 JavaScript 是動態類型、代碼無需編譯能夠在 JS Engine 直接運行(譯者注:Java 代碼須要編譯成機器碼後在 JVM 執行),同時它尚未類。全部的幾乎都是實例(objects)。儘管模擬了class,但其本質仍是函數對象。

你也許注意到了function A上有一個特殊的屬性prototype。這個特殊屬性與 JavaScriptnew操做符一塊兒使用。當使用new操做符建立出來一個實例對象,這個特殊屬性prototype會被複制給該對象的內部[[Prototype]]屬性。舉個例子,當運行var a1 = new A()代碼時,JavaScript(在內存中建立完新實例對象以後且準備運行函數A()以前,運行函數時函數內部的this會指向該對象)會設置:a1.[[Prototype]] = A.prototype
當你以後訪問建立的對象屬性時,JavaScript 首先會檢查屬性是否存在於對象自己,若是不存在,則繼續查找其[[Prototype]]。這意味着你在prototype上定義的屬性實際上被全部實例對象共享,若是你願意,甚至能夠修改prototype,這些改動會同步到全部存在的實例對象中。

若是在上面的例子中,你執行:var a1 = new A(); var a2 = new A();,那麼a1.doSomething就是Object.getPrototypeOf(a1).doSomething,這和你定義的A.prototype.doSomething是同一個對象,因此:Object.getPrototypeOf(a1).doSomething === Object.getPrototypeOf(a2).doSomething === A.prototype.doSomething

簡而言之,prototype是針對類型的,而Object.getPrototypeOf()對於實例對象是一致的。(譯者注:原文爲In short, prototype is for types, while Object.getPrototypeOf() is the same for instances.)。

[[Prototype]]會被遞歸地查找,例如:a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething等等,直到Object.getPrototypeOf返回null

所以,當你執行:

var o = new Foo();

其實是執行:

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

接着若是你訪問:

o.someProp;

JavaScript 會檢查是否 o 上存在自身屬性someProp。若是不存在,繼續檢查Object.getPrototypeOf(o).someProp是否存在,若是還不存在繼續檢查Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp,依次類推。


總結

在編寫基於原型的複雜代碼以前,頗有必要先理解原型式的繼承模型。同時,請注意代碼中原型鏈的長度,而且在必要時將其分解以免可能存在的性能問題。此外,應該杜絕在原生的原型對象上進行擴展,除非是爲了考慮兼容性,例如在老的 JavaScript 引擎上適配新的語言特性。


Tags: Advanced Guide Inheritance JavaScript OOP


本篇文章由一文多發平臺ArtiPub自動發佈

相關文章
相關標籤/搜索