原文來自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 中,正如上面提到,函數也能夠擁有屬性。全部函數都有一個特殊的屬性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.create
和new
操做符一塊兒,使用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
ECMAScript 2015 提出了一系列新的關鍵字用於實現類。包括class
、constructor
、static
、extends
以及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.prototype
的hasOwnProperty
方法。下面來看一個具體的例子,該例子繼續使用上一個圖形的例子:
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
。
常常容易犯的一個錯誤是擴展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及如下版本瀏覽器。 |
prototype
和Object.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自動發佈