五天以前我寫了一個關於ES6標準中Class的文章。在裏面我介紹瞭如何用現有的Javascript來模擬類而且介紹了ES6中類的用法,其實它只是一個語法糖。感謝Om Shakar以及Javascript Room中的各位,個人編程風格從那時候開始發生了改變;就像Dougla Crockford2006年作的同樣,我也學習了不少來徹底理解基於原型的編程方式。javascript
Javascript是一個多樣化的編程語言。它擁有面向對象和函數式的編程特色,你可使用任何一種風格來編寫代碼。然而這兩個編程風格並不能很好的融合。例如,你不沒法同時使用new
(典型的面向對象的特色)和apply
(函數式編程的特色).原型繼承一直都做爲鏈接這兩種風格的橋樑。html
大部分Javascript程序員會告訴你基於類的繼承很差。然而它們中只有不多一部分知道其中的緣由。事實其實是基於類的基礎並無什麼很差。Python是基於類繼承的,而且它是一門很好的編程語言。可是,基於類的繼承並不適合用於Javascript。Python正確的使用了類,它們只有簡單的工廠方法不能當成構造函數使用。而在Javascript中任何函數均可以被當成構造函數使用。java
Javascript中的問題是因爲每一個函數均可以被當成構造函數使用,因此咱們須要區分普通的函數調用和構造函數調用;咱們通常使用new
關鍵字來進行區別。然而,這樣就破壞了Javascript中的函數式特色,由於new
是一個關鍵字而不是函數。於是函數式的特色沒法和對象實例化一塊兒使用。git
function Person(firstname,lastname){ this.firstname = firstname ; this.lastname = lastname ; }
考慮上面這段程序。你能夠經過new
關鍵字來調用Person
方法來建立一個函數Person
的實例:程序員
var author = new Person('Aadit','Shah') ;
然而,沒有任何辦法來使用apply
方法來爲構造函數指定參數列表:github
var author = new Person.apply(null,['Aadit','Shah']);//error
可是,若是new
是一個方法那麼上面的需求就能夠經過下面這種方式實現了:編程
var author = Person.new.apply(Person,['Aadit','Shah']) ;
幸運的是,由於Javascript有原型繼承,因此咱們能夠實現一個new
的函數:設計模式
Function.prototype.new = function () { function functor() { return constructor.apply(this, args); } var args = Array.prototype.slice.call(arguments); functor.prototype = this.prototype; var constructor = this; return new functor; };
在像Java這樣對象只能經過new
關鍵字來實例化的語言中,上面這種方式是不可能實現的。瀏覽器
下面這張表列出了原型繼承相比於基於類的基礎的優勢:安全
基於類的繼承 | 原型繼承 |
---|---|
類是不可變的。在運行時,你沒法修改或者添加新的方法 | 原型是靈活的。它們能夠是不可變的也能夠是可變的 |
類可能會不支持多重繼承 | 對象能夠繼承多個原型對象 |
基於類的繼承比較複雜。你須要使用抽象類,接口和final類等等 | 原型繼承比較簡潔。你只有對象,你只須要對對象進行擴展就能夠了 |
到如今你應該知道爲何我以爲new
關鍵字是不會的了吧---你不能把它和函數式特色混合使用。而後,這並不表明你應該中止使用它。new
關鍵字有合理的用處。可是我仍然建議你不要再使用它了。new
關鍵字掩蓋了Javascript中真正的原型繼承,使得它更像是基於類的繼承。就像Raynos說的:
new
是Javascript在爲了得到流行度而加入與Java相似的語法時期留下來的一個殘留物
Javascript是一個源於Self的基於原型的語言。然而,爲了市場需求,Brendan Eich把它當成Java的小兄弟推出:
而且咱們當時把Javascript當成Java的一個小兄弟,就像在微軟語言家庭中Visual Basic相對於C++同樣。
這個設計決策致使了new
的問題。當人們看到Javascript中的new
關鍵字,他們就想到類,而後當他們使用繼承時就遇到了傻了。就像Douglas Crockford說的:
這個間接的行爲是爲了使傳統的程序員對這門語言更熟悉,可是卻失敗了,就像咱們看到的不多Java程序員選擇了Javascript。Javascript的構造模式並無吸引傳統的人羣。它也掩蓋了Javascript基於原型的本質。結果就是,不多的程序員知道如何高效的使用這門語言
所以我建議中止使用new
關鍵字。Javascript在傳統面向對象假象下面有着更增強大的原型系統。然大部分程序員並無看見這些還處於黑暗中。
原型繼承很簡單。在基於原型的語言中你只有對象。沒有類。有兩種方式來建立一個新對象---「無中生有」對象建立法或者經過現有對象建立。在Javascript中Object.create
方法用來建立新的對象。新的對象以後會經過新的屬性進行擴展。
Javascript中的Object.create
方法用來從0開始建立一個對象,像下面這樣:
var object = Object.create(null) ;
上面例子中新建立的object
沒有任何屬性。
Object.create
方法也能夠克隆一個現有的對象,像下面這樣:
var rectangle = { area : function(){ return this.width * this.height ; } } ; var rect = Object.create(rectangle) ;
上面例子中rect
從rectangle
中繼承了area
方法。同時注意到rectangle
是一個對象字面量。對象字面量是一個簡潔的方法用來建立一個Object.prototype
的克隆而後用新的屬性來擴展它。它等價於:
var rectangle = Object.create(Object.prototype) ; rectangle.area = function(){ return this.width * this.height ; } ;
上面的例子中咱們克隆了rectangle
對象命名爲rect
,可是在咱們使用rect
的area
方法以前咱們須要擴展它的width
和height
屬性,像下面這樣:
rect.width = 5 ; rect.height = 10 ; alert(rect.area()) ;
然而這種方式來建立一個對象的克隆而後擴展它是一個很是傻缺的方法。咱們須要在每一個rectangle
對象的克隆上手動定義width
和height
屬性。若是有一個方法可以爲咱們來完成這些工做就很好了。是否是聽起來有點熟悉?確實是。我要來講說構造函數。咱們把這個函數叫作create
而後在rectangle
對象上定義它:
var rectangle = { create : function(width,height){ var self = Object.create(this) ; self.height = height ; self.width = width ; return self ; } , area : function(){ return this.width * this.height ; } } ; var rect = rectangle.create(5,10) ; alert(rect.area()) ;
等等。這看起來很像Javascript中的正常構造模式:
function Rectangle(width, height) { this.height = height; this.width = width; } ; Rectangle.prototype.area = function () { return this.width * this.height; }; var rect = new Rectangle(5, 10); alert(rect.area());
是的,確實很像。爲了使得Javascript看起來更像Java原型模式被迫屈服於構造模式。所以每一個Javascript中的函數都有一個prototype
對象而後能夠用來做爲構造器(這裏構造器的意思應該是說新的對象是在prototype
對象的基礎上進行構造的)。new
關鍵字容許咱們把函數當作構造函數使用。它會克隆構造函數的prototype
屬性而後把它綁定到this
對象中,若是沒有顯式返回對象則會返回this
。
原型模式和構造模式都是平等的。所以你也許會懷疑爲何有人會困擾因而否應該使用原型模式而不是構造模式。畢竟構造模式比原型模式更加簡潔。可是原型模式相比構造模式有許多優點。具體以下:
構造模式 | 原型模式 |
---|---|
函數式特色沒法與new 關鍵字一塊兒使用 |
函數式特色能夠與create 結合使用 |
忘記使用new 會致使沒法預期的bug而且會污染全局變量 |
因爲create 是一個函數,因此程序老是會按照預期工做 |
使用構造函數的原型繼承比較複雜而且混亂 | 使用原型的原型繼承簡潔易懂 |
最後一點可能須要解釋一下。使用構造函數的原型繼承相比使用原型的原型繼承更加複雜,咱們先看看使用原型的原型繼承:
var square = Object.create(rectangle); square.create = function (side) { return rectangle.create.call(this, side, side); } ; var sq = square.create(5) ; alert(sq.area()) ;
上面的代碼很容易理解。首先咱們建立一個rectangle
的克隆而後命名爲square
。接着咱們用新的create
方法重寫square
對象的create
方法。最終咱們重新的create
方法中調用rectangle
的create
函數而且返回對象。相反的,使用構造函數的原型繼承像下面這樣:
function Square(){ Rectangle.call(this,side,side) ; } ; Square.prototype = Object.create(Rectangle.prototype) ; Square.prototype.constructor = Square ; var sq = new Square(5) ; alert(sq.area()) ;
固然,構造函數的方式更簡單。而後這樣的話,向一個不瞭解狀況的人解釋原型繼承就變得很是困難。若是想一個瞭解類繼承的人解釋則會更加困難。
當使用原型模式時一個對象繼承自另外一個對象就變得很明顯。當使用方法構造模式時就沒有這麼明顯,由於你須要根據其餘構造函數來考慮構造繼承。
在上面的例子中咱們建立一個rectangle
的克隆而後命名爲square
。而後咱們利用新的create
屬性擴展它,重寫繼承自rectangle
對象的create
方法。若是把這兩個操做合併成一個就很好了,就像對象字面量是用來建立Object.prototype
的克隆而後用新的屬性擴展它。這個操做叫作extend
,能夠像下面這樣實現:
Object.prototype.extend = function(extension){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; for(var property in extension){ if(hasOwnProperty.call(extension,property) || typeof obejct[property] === 'undefined') //這段代碼有問題,按照文章意思,這裏應該使用深複製,而不是簡單的淺複製,deepClone(extension[property],object[property]),deepClone的實現能夠看我以前關於繼承的博客 object[properyty] = extension[property] ; } return object ; } ;
譯者注:我以爲博主這裏的實現有點不符合邏輯,正常
extend
的實現應該是能夠配置當被擴展對象和用來擴展的對象屬性重複時是否覆蓋原有屬性,而博主的實現就只是簡單的覆蓋。同時博主的實如今if
判斷中的作法我的以爲是值得學習的,首先判斷extension
屬性是不是對象自身的,若是是就直接複製到object
上,不然再判斷object
上是否有這個屬性,若是沒有那麼也會把屬性複製到object
上,這種實現的結果就使得被擴展的對象不只僅只擴展了extension
中的屬性,還包括了extension
原型中的屬性。不難理解,extension
原型中的屬性會在extension
中表現出來,因此它們也應該做爲extension
所具備的特性而被用來擴展object
。因此我對這個方法進行了改寫:
Object.prototype.extend = function(extension,override){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; for(var property in extension){ if(hasOwnProperty.call(extension,property) || typeof object[property] === 'undefined'){ if(object[property] !== 'undefined'){ if(override){ deepClone(extension[property],object[property]) ; } }else{ deepClone(extension[property],object[property]) ; } } } };
利用上面的extend
方法,咱們能夠重寫square
的代碼:
var square = rectangle.extend({ create : function(side){ return rectangle.create.call(this,side,side) ; } }) ; var sq = square.create(5) ; alert(sq.area()) ;
extend
方法是原型繼承中惟一須要的操做。它是Object.create
函數的超集,所以它能夠用在對象的建立和擴展上。所以咱們能夠用extend
來重寫rectangle
,使得create
函數更加結構化看起來就像模塊模式。
var rectangle = { create : function(width,height){ return this.extend({ height : height , width : width }) ; } } ; var rect = rectangle.create(5,10) ; alert(rect.area()) ;
一些人可能已經注意到extend
函數返回的對象其實是繼承了兩個對象的屬性,一個是被擴展的對象,另外一個是用來擴展的對象。另外從兩個對象繼承屬性的方式也不同。第一種狀況下是經過委派來繼承屬性(也就是使用Object.create()
來繼承屬性),第二種狀況下使用合併屬性的方式來繼承屬性。
不少Javascript程序員對於差異繼承比較熟悉。維基百科是這麼解釋的:
大部分對象是從其餘更通常的對象中獲得的,只是在一些很小的地方進行了修改。每一個對象一般在內部維護一個指向其餘對象的引用列表,這些對象就是該對象自己進行差別化繼承的對象。
Javascript中的原型繼承是基於差別化繼承的。每一個對象都有個內部指針叫作[[proto]] (在大部分瀏覽器中能夠經過__proto__屬性訪問),這個指針指向對象的原型。多個對象之間經過內部[[proto]]屬性連接起來造成了原型鏈,鏈的最後指向null
。
當你試圖獲取一個對象的屬性時Javascript引擎會首先查找對象自身的屬性。若是在對象上沒找到該屬性,那麼它就會去對象的原型中去查找。以此類推,它會沿着原型鏈一直查找知道找到或者到原型鏈的末尾。
function get(object,property){ if(!Object.hasOwnProperty.call(object,property)){ var prototype = Object.getPrototypeOf(object) ; if(prototype) return get(prototype,property) ; }else{ return object[property] ; } } ;
Javascript中屬性查找的過程就像上面的程序那樣。
大多數Javascript程序員會以爲複製一個對象的屬性到另外一個對象上並非一個正確的繼承的方式,由於任何對原始對象的修改都不會反映在克隆的對象上。五天前我會贊成這個觀點。然而如今我相信合併式繼承是原型繼承的一種正確方式。對於原始對象的修改能夠發送到它的副原本實現真正的原型繼承。
合併式繼承和代理有他們的優勢和缺點。下表列出了它們的優缺點:
代理 | 合併 |
---|---|
任何對於原型的修改都會反映在全部副本上 | 任何對於原型的修改都須要手動更新到副本中 |
屬性查找效率較低由於須要進行原型鏈查找 | 屬性查找更搞笑由於繼承的屬性是經過複製的方式附加在對象自己的 |
使用Object.create() 方法只能繼承單一對象 |
對象能夠從任意數量的對象中經過複製繼承屬性 |
上表中最後一點告訴咱們對象能夠經過合併的方式從多個原型中繼承屬性。這是一個重要的特色由於這證實原型繼承比Java中的類繼承更強大而且與C++中的類繼承同樣強大。爲了實現多重繼承,你只須要修改extend
方法來從多個原型中複製屬性。
Object.prototype.extend = function(){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; var length = arguments.length ; var index = length ; while(index){ var extension = arguments[length - (index--)] ; for(var property in extension){ if(hasOwnProperty.call(extension,property)|| typeof object[property] === 'undefined'){ //這裏一樣應該使用深複製 object[property] = extension[property] ; } } } return object; } ;
多重繼承是很是有用的由於它提升了代碼的可重用性和模塊化。對象經過委派繼承一個原型對象而後經過合併繼承其餘屬性。好比說你有一個事件發射器的原型,像下面這樣:
var eventEmitter = { on : function(event,listener){ if(typeof this[event] !== 'undefined') this[event].push(listener) ; else this[event] = [listener] ; } , emit : function(event){ if(typeof this[event] !== 'undefined'){ var listeners = this[event] ; var length = listeners.length,index = length ; var args = Array.prototype.slice.call(arguments,1) ; while(index){ var listener = listeners[length - (index--)] ; listener.apply(this,args) ; } } } } ;
如今你但願square
表現得像一個事件發射器。由於square
已經經過委派的方式繼承了rectangle
,因此它必須經過合併的方式繼承eventEmitter
。這個修改能夠很容易地經過使用extend
方法實現:
var square = rectangle.extend(eventEmitter,{ create : function(side){ return rectangle.create.call(this,side,side) ; } , resize : function(newSize){ var oldSize = this.width ; this.width = this.height = newSize ; this.emit('resize',oldSize,newSize) ; } }) ; var sq = square.create(5) ; sq.on('resize',function(oldSize,newSize){ alert('sq resized from ' + oldSize + 'to' + newSize + '.') ; }) ; sq.resize(10) ; alert(sq.area()) ;
在Java中是不可能實現上面的程序的,由於它不支持多重繼承。相應的你必須另外再建立一個EventEmitter
類或者使用一個EventEmitter
接口而且在每一個實現該接口的類中分別實現on
和emit
方法。固然你在C++中不須要面對這個問題。咱們都知道Java sucks(呵呵呵)。
在上面的例子中你確定注意到eventEmitter
原型並無一個create
方法。這是由於你不該該直接建立一個eventEmitter
對象。相反eventEmitter
是用來做爲其餘原型的原型。這類原型稱爲mixin。它們等價於抽象類。mixin用來經過提供一系列可重用的方法來擴展對象的功能。
然而有時候mixin須要私有的狀態。例如eventEmitter
若是可以把它的事件監聽者列表放在私有變量中而不是放在this
對象上會安全得多。可是mixin沒有create
方法來封裝私有狀態。所以咱們須要爲mixin建立一個藍圖(blueprint)來建立閉包。藍圖(blueprint)看起來會像是構造函數可是它們並不用像構造函數那樣使用。例如:
function eventEmitter(){ var evnets = Object.create(null) ; this.on = function(event,listener){ if(typeof events[event] !== 'undefined') events[event].push(listener) ; else events[event] = [listener] ; } ; this.emit = function(event){ if(typeof events[event] !== 'undefined'){ var listeners = events[event] ; var length = listeners.length ,index = length ; var args = Array.prototype.slice.call(arguments,1) ; } } ; } ;
一個藍圖用來在一個對象建立以後經過合併來擴展它(我以爲有點像裝飾者模式)。Eric Elliot把它們叫作閉包原型。咱們可使用藍圖版本的eventEmitter
來重寫square
的代碼,以下:
var square = rectangle.extend({ create : function(side){ var self = rectangle.create.call(this,side,side) ; eventEmitter.call(self) ; return self ; } , resize : function(newSize){ var oldSize = this.width ; this.width = this.height = newSize ; this.emit('resize',oldSize,newSize) ; } }) ; var sq = square.create(5) ; sq.on('resize',function(oldSize,newSize){ alert('sq resized from ' + oldSize + 'to' + newSize + '.') ; }) ; sq.resize(10) ; alert(sq.area()) ;
藍圖在Javascript中是獨一無二的。它是一個很強大的特性。然而它們也有本身的缺點。下表列出了mixin和藍圖的優缺點:
Mixin | 藍圖 |
---|---|
它們用來擴展對象的原型。所以對象共享同一個原型 | 它們用來擴展新建立的對象。所以每一個對象都是在本身對象自己進行修改 |
由於缺乏封裝方法因此不存在私有狀態 | 它們是函數,因此能夠封裝私有狀態 |
它們是靜態原型而且不能被自定義 | 它們能夠傳遞參數來自定義對象,能夠向藍圖函數傳遞一些用來自定義的參數 |
許多Javascript程序員會以爲使用原型模式來繼承違背了語言的精髓。他們更偏向於構造模式由於他們以爲經過構造函數建立的對象纔是真正的實例,由於instanceof
操做會返回true
。然而,這個爭論是沒有意義的,由於instanceof
操做能夠像下面這樣實現:
Object.prototype.instanceof = function(prototype){ var object = this ; do{ if(object === prototype) return true ; var object = Object.getPrototypeOf(object) ; }while(object) ; return false ; }
這個instanceof
方法如今能夠被用來測試一個對象是不是經過委派從一個原型繼承的。例如:
sq.instanceof(square) ;
然而仍是沒有辦法判斷一個對象是不是經過合併的方式從一個原型繼承的,由於實例的關聯信息丟失了。爲了解決這個問題咱們將一個原型的全部克隆的引用保存在原型自身中,而後使用這個信息來判斷一個對象是不是一個原型的實例。這個能夠經過修改extend
方法來實現:
Object.prototype.extend = function(){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; var length = arguments.lenght ; var index = length ; while(index){ var extension = arguments[length - (index--)] ; for(var property in extension){ if(property !== 'clones' && hasOwnProperty.call(extension,property) || typeof object[property] === 'undefined') object[property] = extension[property] ; if(hasOwnProperty.call(extension,'clones')}) extension.clones.unshift(object) ; else extension.clones = [object] ; } } return object; } ;
經過合併繼承自原型的對象造成了一個克隆樹,這些樹從根對象開始而後向下一直到葉子對象。一個克隆鏈是一個從根對象到葉子對象的單一路徑,這跟遍歷原型鏈很類似。咱們可使用這個信息來判斷一個對象是不是經過合併繼承自一個原型。
Object.prototype.instanceof = function(prototype){ if (Object.hasOwnProperty.call(prototype, "clones")) var clones = prototype.clones; var object = this; do { if (object === prototype || clones && clones.indexOf(object) >= 0) return true; var object = Object.getPrototypeOf(o bject); } while (object); return false; } ;
這個instanceof
方法如今能夠用來判斷一個對象是不是經過合併繼承自一個原型。例如:
sq.instanceof(eventEmitter);
在上面的程序中instanceof
會返回true
若是我媽使用mixin版本的eventEmitter
。然而若是咱們使用藍圖版本的eventEmitter
它會返回false
。爲了解決這個問題我建立了一個藍圖函數,這個函數接收一個藍圖做爲參數,向它添加一個clones
屬性而後返回一個記錄了它的克隆的新藍圖:
function blueprint(f){ var g = function(){ f.apply(this,arguments) ; g.clones.unshift(this) ; } ; g.clones = [] ; return g ; } ; var eventEmitter = blueprint(function(){ var events = Object.create(null); this.on = function (event, listener) { if (typeof events[event] !== "undefined") events[event].push(listener); else events[event] = [listener]; }; this.emit = function (event) { if (typeof events[event] !== "undefined") { var listeners = events[event]; var length = listeners.length, index = length; var args = Array.prototype.slice.call(arguments, 1); while (index) { var listener = listeners[length - (index--)]; listener.apply(this, args); } } }; }) ;
上面例子中的clones
屬性有雙重做用。它能夠用來判斷一個對象是不是經過合併繼承自一個原型的,而後他能夠用來發送原型改變給全部它的克隆。原型繼承相比類繼承最大的優點就是你能夠修改一個原型在它建立以後。爲了使克隆能夠繼承對於原型的修改,咱們建立了一個叫作define
的函數:
Object.prototype.define = function (property, value) { this[property] = value; if (Object.hasOwnProperty.call(this, "clones")) { var clones = this.clones; var length = clones.length; while (length) { var clone = clones[--length]; if (typeof clone[property] === "undefined") clone.define(property, value); } } };
如今咱們能夠修改原型而後這個修改會反映在全部的克隆上。例如咱們能夠建立建立一個別名addEventListener
針對eventEmitter
上的on
方法:
var square = rectangle.extend(eventEmitter, { create: function (side) { return rectangle.create.call(this, side, side); }, resize: function (newSize) { var oldSize = this.width; this.width = this.height = newSize; this.emit("resize", oldSize, newSize); } }); var sq = square.create(5); eventEmitter.define("addEventListener", eventEmitter.on); sq.addEventListener("resize", function (oldSize, newSize) { alert("sq resized from " + oldSize + " to " + newSize + "."); }); sq.resize(10); alert(sq.area());
藍圖須要特別注意。儘管對於藍圖的修改會被髮送到它的克隆,可是藍圖的新的克隆並不會反映這些修改。幸運的是這個問題的解決方法很簡單。咱們只須要對blueprint
方法進行小小的修改,而後任何對於藍圖的修改就會反映在克隆上了。
function blueprint(f) { var g = function () { f.apply(this, arguments); g.clones.unshift(this); var hasOwnProperty = Object.hasOwnProperty; for (var property in g) if (property !== "clones" && hasOwnProperty.call(g, property)) this[property] = g[property]; }; g.clones = []; return g; };
恭喜你。若是你讀完了整篇文章而且理解了我所說的東西,你如今就瞭解了 原型繼承而且爲何它很重要。很感謝大家看完了這篇文章。我但願這個博客能幫到大家。原型繼承是強大的而且值得更多的信任。而後大部分人歷來不明白這個由於Javascript中的原型繼承被構造模式所掩蓋了。
這篇文章針對幾種繼承方式進行了對比。文章中說到的幾種擴展的方法我以爲是比較有用的。藍圖(blueprint,這個實在不知道該怎麼翻譯)的擴展方式比較像設計模式中的裝飾者模式,經過函數對對象進行擴展,這個是一種比較好玩的擴展方式,能夠跟原型繼承配合使用。另外文中提到了new
關鍵字的弊端,我的以爲主要的緣由仍是new
關鍵字的出現掩蓋了Javascript自己原型繼承的特色,人們天然而然就會想到傳統的類繼承,這樣就沒法發揮原型繼承的最大威力。最後說到的屬性修改傳播的問題也挺有意思的,應該會有相應的應用場景。總之,我以爲原型繼承相比於傳統的類繼承提供了更大的靈活性,能夠給咱們開發者提供很大的發揮空間,不過無論怎樣,到最後仍是要涉及到基本的原型繼承的原理上,因此掌握了原型繼承的原理就能夠根據不一樣的應用場景使用各類各樣的擴展方式。
原文地址:http://aaditmshah.github.io/why-prototypal-inheritance-matters/