對於 OO 語言,有一句話叫「Everything is object」,雖然 JavaScript 不是嚴格意義上的面嚮對象語言,但若是想要理解 JS 中的繼承,這句話必須時刻銘記於心。node
JS 的語法很是靈活,因此有人以爲它簡單,由於怎麼寫都是對的;也有人以爲它難,由於很難解釋某些語法的設計,誰能告訴我爲何 typeof null 是 object 而 typeof undefined 是 undefined 嗎?而且這是在 null == undefined 的前提下。不少咱們自認爲「懂」了的知識點,細細琢磨起來,仍是會發現有不少盲點,「無畏源於無知」吧……編程
既然是講繼承,天然是從最簡單的對象提及:瀏覽器
var dog = { name: 'tom' }
這即是對象直接量了。每個對象直接量都是 Object 的子類,即app
dog instanceof Object; // true
JS 中的構造函數與普通函數並無什麼兩樣,只不過在調用時,前面加上了 new 關鍵字,就當成是構造函數了。函數
function Dog(name) { this.name = name; } var dog = new Dog('tom'); dog instanceof Dog; // true
兩個問題,第一,不加 new 關鍵字有什麼後果?測試
那麼 Dog 函數中的 this 在上下文(Context)中被解釋爲全局變量,具體在瀏覽器端的話是 window 對象,在 node 環境下是一個 global 對象。this
第二,dog 的值是什麼?很簡單,undefined 。Do>g 函數沒有返回任何值,執行結束後,dog 的值天然是 undefined 。spa
關於 new 的過程,這裏也順便介紹一下,這個對後面理解原型(prototype)有很大的幫助:prototype
建立一個空的對象,僅包含 Object 的屬性和方法。
將 prototype 中的屬性和方法建立一份引用,賦給新對象。
將 this 上的屬性和方法新建一份,賦給新對象。
返回 this 對象,忽略 return 語句。
須要明確的是,prototype 上的屬性和方法是實例間共享的,this 上的屬性和方法是每一個實例獨有的。設計
如今爲 Dog 函數加上 prototype,看一個例子:
function Dog(name) { this.name = name; this.bark = function() {}; } Dog.prototype.jump = function() {}; Dog.prototype.species = 'Labrador'; Dog.prototype.teeth = ['1', '2', '3', '4']; var dog1 = new Dog('tom'), dog2 = new Dog('jerry'); dog1.bark !== dog2.bark; // true dog1.jump === dog2.jump; // true dog1.teeth.push('5'); dog2.teeth; // ['1', '2', '3', '4', '5']
看到有註釋的那三行應該能夠明白「引用」和「新建」的區別了。
那麼咱們常常說到的「原型鏈」究竟是什麼呢?這個術語出如今繼承當中,它用於表示對象實例中的屬性和方法來自於何處(哪一個父類)。好吧,這是筆者的解釋。
- Object bark: Dog/this.bark() name: 'tom' - __proto__: Object jump: Dog.prototype.jump() species: 'Labrador' + teeth: Array[4] + constructor: Dog() + __proto__: Object
上面的是 dog1 的原型鏈,不知道夠不夠直觀地描述「鏈」這個概念。
其中,bark 和 name 是定義在 this 中的,因此最頂層能夠看到它倆。
而後,每個對象都會有一個 proto 屬性(IE 11+),它表示定義在原型上的屬性和方法,因此 jump、species 和 teeth 天然就在這兒了。
最後就一直向上找 proto 中的屬性和方法。
繼承在編程中有兩種說法,一個叫 inherit,另外一個是 extend 。前者是嚴格意義上的繼承,即存在父子關係,然後者僅僅是一個類擴展了另外一個類的屬性和方法。那麼 call 和 apply 就屬於後者的範疇。怎麼說?
function Animal(gender) { this.gender = gender; } function Dog(name, gender) { Animal.call(this, gender); this.name = name; } var dog = new Dog('tom', 'male'); dog instanceof Animal; // false
雖然在 dog 對象中有 gender 屬性,但 dog 卻不是 Animal 類型。甚至,這種方式只能「繼承」父類在 this 上定義的屬性和方法,並不能繼承 Animal.prototype 中的屬性和方法。
要實現繼承,必須包含「原型」的概念。下面是很經常使用的繼承方式。
function Dog(name) { Animal.call(this); } Dog.prototype = new Animal(); // 先假設 Animal 函數沒有參數 Dog.prototype.constructor = Dog; var dog = new Dog('tom'); dog instanceof Animal; // true
繼承的結果有兩個:
得到父類的屬性和方法;
正確經過 instanceof 的測試。
prototype 也是對象,它是建立實例時的裝配機,這個在前面有提過。new Animal() 的值包含 Animal 實例全部的屬性和方法,既然它賦給了 Dog 的 prototype,那麼 Dog 的實例天然就得到了父類的全部屬性和方法。
而且,經過這個例子能夠知道,改變 Dog 的 prototype 屬性能夠改變 instanceof 的測試結果,也就是改變了父類。
而後,爲何要在 Dog 的構造函數中調用 Animal.call(this)?
由於 Animal 中可能在 this 上定義了方法和函數,若是沒有這句話,那麼全部的這一切都會給到 Dog 的 prototype 上,根據前面的知識咱們知道,prototype 中的屬性和方法在實例間是共享的。
咱們但願將這些屬性和方法依然保留在實例自身的空間,而不是共享,所以須要重寫一份。
至於爲何要修改 constructor,只能說是爲了正確的顯示原型鏈吧,它並不會影響 instanceof 的判斷。或者有其餘更深的道理我並不知道……
上面的繼承方式已經近乎完美了,除了兩點:
Animal 有構造參數,而且使用了這些參數怎麼辦?
在 Dog.prototype 中多了一份定義在 Animal 實例中冗餘的屬性和方法。
function Animal(name) { name.doSomething(); } function Dog(name) { Animal.call(this, name); } Dog.prototype = new Animal(); // 因爲沒有傳入name變量,在調用Animal的構造函數時,會出錯 Dog.prototype.constructor = Dog;
這個問題能夠經過一個空對象來解決(改自 Douglas Crockford)。
function DummyAnimal() {} DummyAnimal.prototype = Animal.prototype; Dog.prototype = new DummyAnimal(); Dog.prototype.constructor = Dog;
他的原始方法是下面的 object:
function object(o) { function F() {} F.prototype = o; return new F(); } Dog.prototype = object(Animal.prototype); Dog.prototype.constructor = Dog;
如今就只剩下一個問題了,如何把冗餘屬性和方法去掉?
其實,從第 3 小節介紹原型的時候就提到了 proto 屬性,instanceof 運算符是經過它來判斷是否屬於某個類型的。
因此咱們能夠這麼繼承:
function Dog() { Animal.call(this); } Dog.prototype = { __proto__: Animal.prototype, constructor: Dog };
若是不考慮兼容性的話,這應該是從 OO 的角度來看最貼切的繼承方式了。
這個方式也只能稱之爲 extend 而不是 inherit,因此也不必展開說。
像 Backbone.Model.extend、jQuery.extend 或者 _.extend 都是拷貝繼承,能夠稍微看一下它們是怎麼實現的。(或者等我本身再好好研究以後過來把這部分補上吧)
當咱們在討論繼承的實現方式時,給個人感受就像孔乙己在炫耀「茴香豆」的「茴」有幾種寫法同樣。繼承是 JS 中佔比很大的一塊內容,因此不少庫都有本身的實現方式,它們並無使用我認爲的「最貼切」的方法,爲何?JS 就是 JS,它生來就設計得很是靈活,因此咱們爲何不利用這個特性,而非得將 OO 的作法強加於它呢?
經過繼承,咱們更多的是但願得到父類的屬性和方法,至因而否要保證嚴格的父類/子類關係,不少時候並不在意,而拷貝繼承最能體現這一點。對於基於原型的繼承,會在代碼中看到各類用 function 定義的類型,而拷貝繼承更通用,它只是將一個對象的屬性和方法拷貝(擴展)到另外一個對象而已,並不關心原型鏈是什麼。
固然,在我鼓吹拷貝繼承多麼多麼好時,基於原型的繼承天然有它不可取代的理由。因此具體問題得具體分析,當具體的使用場景沒定下來時,就不存在最好的方法。
我的看法,能幫助你們更加理解繼承一點就最好,若是有什麼不對的,請多多指教!