【翻譯】JavaScript原型繼承工作原理
譯者按:這篇文章原是出自著名的前端博主阮一峰一篇關於JS原型繼承的文章:Javascript繼承機制的設計思想中的引用。這篇文章對於原型繼承講解詳細,令人讀之有撥雲見日之感,作者在文章裡說:
我一直很難理解Javascript語言的繼承機制。
它沒有“子類”和“父類”的概念,也沒有“類”(class)和“例項”(instance)的區分,全靠一種很奇特的“原型鏈”(prototype chain)模式,來實現繼承。
我花了很多時間,學習這個部分,還做了很多筆記。但是都屬於強行記憶,無法從根本上理解。
直到昨天,我讀到法國程式設計師Vjeux的解釋,才恍然大悟,完全明白了Javascript為什麼這樣設計。
不過說來慚愧,本人物件導向學的不是很好,對於傳統的繼承思想其實理解的並不深刻,所以JS的在程式語言裡特立獨行的原型繼承對我來說就是第一個深入理解的繼承思想。我也特別想知道這篇令作者恍然大悟的文章究竟講的如何,於是特意翻譯一下,以饗讀者。
原文連結:Javascript – How Prototypal Inheritance really works
JavaScript採用原型繼承這事兒是眾所皆知的,但由於它預設只提供了一個實現的例項,也就是 new
運算子,因此對於它的解釋總是令人困惑。這篇文章旨在闡明什麼是原型繼承以及在JavaScript中究竟如何使用原型繼承。
原型繼承的定義
當你閱讀關於JS原型繼承的解釋時,你時常會看到以下這段文字:
當查詢一個物件的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性為止。——出自JavaScript祕密花園
大多數JavaScript的實現用 __proto__
屬性來表示一個物件的原型鏈。在這篇文章裡我們將看到 __proto__
與 prototype
的區別何在。
注:__proto__
是一個不應在你程式碼中出現的非正規的用法,這裡僅僅用它來解釋JavaScript原型繼承的工作原理。
以下程式碼展示了JS引擎如何查詢屬性:
function getProperty(obj, prop) {
if (obj.hasOwnProperty(prop))
return obj[prop]
else if (obj.__proto__ !== null)
return getProperty(obj.__proto__, prop)
else
return undefined
}
讓我們舉一個常見的例子:二維點,擁有二維座標 x
y
,同似擁有一個 print
方法。
用之前我們說過的原型繼承的定義,我們建立一個物件 Point
,擁有三個屬性:x
,y
和 print
。為了能建立一個新的二維點,我們需要建立一個新的物件,讓他其中的 __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 想讓JS和傳統的物件導向的程式語言差不太多,如Java和C++。在這些語言裡,我們採用 new
運算子來給類例項化一個新的物件。所以他在JS裡寫了一個 new
運算子。
- C++裡有用來初始化例項屬性的建構函式概念,因此
new
運算子必須針對函式。 - 我們需要將物件的方法放到一個地方去,既然我們在用原型語言,我們就把它放到函式的原型屬性中去。
new
運算子接受一個函式 F
及其引數:new F(arguments...)
。這一過程分為三步:
- 建立類的例項。這步是把一個空的物件的
__proto__
屬性設定為F.prototype
。 - 初始化例項。函式
F
被傳入引數並呼叫,關鍵字this
被設定為該例項。 - 返回例項。
現在我們知道了 new
是怎麼工作的,我們可以用JS程式碼實現一下:
function New (f) {
var n = { '__proto__': f.prototype }; /*第一步*/
return function () {
f.apply(n, arguments); /*第二步*/
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中真正的原型繼承
JS的ECMA規範只允許我們採用 new
運算子來進行原型繼承。但是大宗師 Douglas Crockford 卻發現了一種可以利用 new
來實現真正的原型繼承的方式!他寫下了 Object.create
函式如下:
Object.create = function (parent) {
function F() {}
F.prototype = parent;
return new F();
};
這看起來蠻奇怪的,但卻是相當的簡潔:它建立了新的物件,並將其原型設定為你想設定的任意值。如果我們允許使用 __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
結論
我們已經瞭解了JS原型繼承是什麼,以及JS如何用特定的方式來實現之。然而使用真正的原型繼承(如 Object.create
以及 __proto__
)還是存在以下缺點:
- 標準性差:
__proto__
不是一個標準用法,甚至是一個不贊成使用的用法。同時原生態的Object.create
和道爺寫的原版也不盡相同。 - 優化性差: 不論是原生的還是自定義的
Object.create
,其效能都遠沒有new
的優化程度高,前者要比後者慢高達10倍。
附圖,ECMA中的原型繼承解釋圖:
譯者的理解
實際上我覺得這篇文章所說還是十分的晦澀,沒有阮一峰的原文寫的深入淺出。這裡還是推薦要了解原型繼承和JS物件導向程式設計的讀者閱讀阮一峰的四篇博文:
- Javascript 繼承機制的設計思想
- Javascript 物件導向程式設計(一):封裝
- Javascript 物件導向程式設計(二):建構函式的繼承
- Javascript 物件導向程式設計(三):非建構函式的繼承
這裡簡單的說一下我的理解:
為什麼不推薦使用原型鏈
__proto__
的寫法?因為這樣會對下游的子類開放對整個原型鏈的操作許可權,也就是在子類中就可以對由上游父類產生的原型鏈進行修改,這一點是十分危險的。比如:var P = new function(){} P.prototype.name = 'parent' var a = new P() a.name // 'parent' a.__proto__ === P.prototype //true a.__proto__.name = 'child' P.prototype.name // 被修改為'child'
原型繼承到底繼承了什麼?實際上就是繼承了建構函式和原型鏈兩個東西。其中建構函式繼承是用
apply()
實現的,這就意味著僅僅是把父類裡面的屬性複製了一遍,對其進行任何的更改,都不會影響其他的例項,在繼承之後對父類進行任何更改也不會影響其子類。而對於原型鏈的繼承,則是表示子類和父類共用原型鏈上的屬性和方法,對於原型鏈的更改只能從上游源頭進行修改,當然子類可以重寫父類的方法,不過這樣做實際上就是先給子類增添一個重名的方法,而導致JS引擎先呼叫此方法而不去呼叫原型鏈的方法。大宗師的程式碼到底幹了什麼?道爺的程式碼主要是把剛才的建構函式繼承都拋棄了,把所有父類的屬性全部放在原型鏈上,讓子類直接引用。這樣是相當的節省記憶體的,這時子類如果想更改某一屬性,則就會遵循2中所說的重寫的方法。而且道爺這樣寫,父類就可以是任意的物件,而不必要非是函式,理解起來比JS晦澀的原生繼承好的多。
相關文章
- 原型繼承(翻譯 vjeux 文章)原型繼承UX
- 深入 JavaScript 原型繼承原理——babel 編譯碼解讀JavaScript原型繼承Babel編譯
- 原型,繼承——原型繼承原型繼承
- javascript - 繼承與原型鏈JavaScript繼承原型
- javascript的原型和繼承JavaScript原型繼承
- javascript原型鏈及繼承JavaScript原型繼承
- Javascript繼承4:潔淨的繼承者—-原型式繼承JavaScript繼承原型
- 【機制】JavaScript的原型、原型鏈、繼承JavaScript原型繼承
- 圖解JavaScript原型鏈繼承圖解JavaScript原型繼承
- javascript原型鏈繼承的使用JavaScript原型繼承
- 深入JavaScript繼承原理JavaScript繼承
- 搞懂 JavaScript 繼承原理JavaScript繼承
- javascript基礎-原型鏈與繼承JavaScript原型繼承
- JavaScript原型與繼承的祕密JavaScript原型繼承
- 白話JavaScript原型鏈和繼承JavaScript原型繼承
- [JavaScript]原型、原型鏈、建構函式與繼承JavaScript原型函式繼承
- 說清楚javascript物件導向、原型、繼承JavaScript物件原型繼承
- JavaScript物件導向 ~ 原型和繼承(1)JavaScript物件原型繼承
- 深入理解JavaScript原型鏈與繼承JavaScript原型繼承
- 一次掌握 JavaScript 原型與繼承JavaScript原型繼承
- 原型、原型鏈與繼承原型繼承
- 原型和繼承原型繼承
- 物件-原型-繼承物件原型繼承
- JavaScript 型別、原型與繼承學習筆記JavaScript型別原型繼承筆記
- JS原型鏈繼承JS原型繼承
- 原型繼承:子類原型繼承
- Javascript 中實現物件原型繼承的三種方式JavaScript物件原型繼承
- JavaScript繼承JavaScript繼承
- javascript:繼承JavaScript繼承
- JavaScript 繼承JavaScript繼承
- [非專業翻譯] Mapster - 對映配置繼承繼承
- 徹底搞懂原型、原型鏈和繼承原型繼承
- 原型、原型鏈、new做了什麼、繼承原型繼承
- 建構函式、原型、原型鏈、繼承函式原型繼承
- javascript 筆記03(建立物件/原型模式/js 繼承/BOM)JavaScript筆記物件原型模式JS繼承
- JavaScript 原型鏈與繼承問答(第一天)JavaScript原型繼承
- javascript組合繼承的基本原理JavaScript繼承
- JavaScript class 繼承JavaScript繼承
- JavaScript extends 繼承JavaScript繼承