幾乎全部語言都有面向對象的概念,JavaScript 的面向對象實質是基於原型的對象系統。說到面向對象,不得不提的就是繼承。數組
一個沒有其餘語言經驗的人要更容易理解 JavaScript 的繼承。
不一樣於其餘語言的類的繼承,JavaScript 中使用的是原型繼承,不過從表面上看更像是基於類的繼承,緣由多是由於 new 關鍵字的使用。new 關鍵字是用來調用構造函數的,一個函數之因此稱爲構造函數,並非由於函數自己有什麼特性,而是由於 new 。也就是說只有經過 new 調用的函數纔可能成爲構造函數。
new 既然這麼神奇,有必要探究下內部到底實現了什麼。app
下面是簡易的實現代碼:函數
function myNew(constructor, param) { // constructor 就是構造函數,param 模擬構造函數的參數,這裏只用一個參數舉例 // 建立一個空對象,且這個空對象繼承構造函數的 prototype 屬性 const obj = Object.create(constructor.prototype) // 將構造函數的 this 指向 obj,執行構造函數獲得返回結果 const result = constructor.apply(obj, param) // 若是造函數執行後,返回結果是對象類型,就直接返回,不然返回 obj 對象 return (typeof result === 'object' && result != null) ? result : obj } function Animal(species) { this.species = species } const cat = myNew(Animal, '貓科動物') console.log(cat) // {species: "貓科動物"}
結合上面 new 實現的三步,再看代碼就比較容易理解了。
須要注意的就是,若是構造函數有顯式返回值,而且返回值類型爲對象。那麼構造函數返回的結果再也不是目標實例,而是這個顯式的返回值。this
JavaScript 實現繼承的方式有多種,各有優缺點,下面就將常見的幾種方式一一列出。prototype
1、構造函數的繼承方式code
function Animal(species) { this.species = species || "動物" } function Cat(name, color, species) { Animal.call(this, species) this.name = name this.color = color } Animal.prototype.age = 10 var cat1 = new Cat("毛毛", "黑色", "貓科動物") console.log(cat1.species) // 貓科動物 console.log(cat1.age) // undefined
這種繼承的實現方式是在子類構造函數中執行父類構造函數,並將父類構造函數的this指向子類的實例。優勢簡單易懂,可是缺點也很明顯。
==缺點==:沒法繼承父類原型上的屬性和方法。orm
2、原型鏈繼承模式對象
function Animal() { this.species = "動物" this.list = [1, 2, 3] } function Cat(name, color) { this.name = name this.color = color } // 將Cat的prototype對象指向一個Animal的實例 // 它至關於徹底覆蓋了 prototype 對象原先的值。 Cat.prototype = new Animal() // 本行下面會有詳細解釋 Cat.prototype.constructor = Cat var cat1 = new Cat("大黃", "黃色") var cat2 = new Cat("小黃", "黑色") //這種方式實現的繼承,不一樣實例的原型對象都指向同一個Animal的實例,訪問屬性的時候,若是實例內沒有該屬性,就會向上找到Cat.prototype(Animal的一個實例)中。 //可是這裏調用cat1.species去賦值,不會向上尋找,而只是在cat1實例中添加一個species屬性,並不會影響cat1原型對象(Animal實例)中的屬性,所以cat2.species的值沒有變化,這種方式並不能說明問題,看下面的代碼 cat1.species = "貓科動物" console.log(cat1.species) // 貓科動物 console.log(cat2.species) // 動物 // 調用數組的push方法,就會順着原型鏈搜索,找到原型對象中的list並修改值,上面說了不一樣實例的原型對象都指向同一個Animal的實例,因此 cat2.list 讀取到的值也是改變後的。 cat1.list.push(4) console.log(cat1.list) // [1,2,3,4] console.log(cat2.list) // [1,2,3,4]
關於Cat.prototype.constructor = Cat
,是給Cat構造函數的原型對象上的constructor屬性從新賦值。
由於任何一個prototype對象都有一個constructor屬性,指向它的構造函數,若是沒有Cat.prototype = new Animal()
時,Cat.prototype.constructor
是指向Cat
的,可是執行了這句代碼後Cat.prototype.constructor
指向Animal
。
至關於繼承
Cat.prototype.constructor === Animal //true
而且,構造函數創造出的每個實例也有一個constructor
屬性,讀取的是構造函數的prototype
對象的constructor
屬性。
至關於ip
cat1.constructor === Cat.prototype.constructor // true
所以,cat1.constructor也指向Animal
cat1.constructor === Animal // true
這樣致使的結果是繼承關係混亂
手動修改了constructor,雖然解決了這個關係混亂的問題,可是代碼中也能夠看到這種實現方式也是有缺點的。
==優勢==:
實例是子類實例,同時也是父類的實例;
實例能夠訪問到父類新增的原型屬性和原型方法;
子類原型共享父類原型,父類原型不共享子類原型;
==缺點==:
繼承的實例屬性,全部子類共享同一個父類實例的實例屬性;
沒法向父類構造函數傳參;
3、原型鏈繼承改版,直接繼承prototype
基於第二種原型鏈方式的改進,想要解決以前方式的缺點。
function Animal() { this.age = 10 } function Cat() {} Animal.prototype.species = "動物" Cat.prototype = Animal.prototype Cat.prototype.constructor = Cat var cat1 = new Cat() console.log(cat1.species) // 動物 console.log(cat1.age) // undefined Cat.prototype.gender = "formall" var a = new Animal() console.log(a.gender) // formall
這種方式跳過new Animal()
直接繼承Animal.prototype
。想象的是不共享同一個父類實例屬性,可是又致使一個問題,Cat.prototype
和Animal.prototype
如今指向了同一個對象,那麼任何對Cat.prototype
的修改,都會體現到Animal.prototype
上。同時子類實例沒法訪問父類實例屬性。
==缺點==:
子類父類共享原型對象;
沒法繼承父類實例屬性;
4、原型鏈繼承改版,利用空對象
先來解決子類父類共享原型對象的問題,實用的辦法是建立一箇中間對象。
function Animal() { this.age = 10 } function Cat() { } var F = function () { } F.prototype = Animal.prototype Cat.prototype = new F() Cat.prototype.constructor = Cat Animal.prototype.species = "動物" Cat.prototype.gender = "formall" var cat1 = new Cat() var a = new Animal() console.log(cat1.species) // 動物 console.log(cat1.age) // undefined console.log(a.gender) // undefined
顯然這種方式解決了子父類共享原型對象的問題,可是沒法繼承父類實例屬性的問題還在。依然不能訪問父類的實例屬性。
==優勢==:
子類添加原型屬性,父類不會更新;
==缺點==:
沒法繼承父類實例屬性;
5、組合繼承(構造函數+原型鏈)
實現了這麼多種繼承,可是每種都有缺點不足。能不能去其糟粕取其精華呢?實現一個較優的繼承方式。
function Animal(species, age) { this.species = species || "動物" this.age = age || 10 this.list = [1,2,3] } function Cat() { Animal.call(this) } Cat.prototype = Object.create(Animal.prototype) Cat.prototype.constructor = Cat var c1 = new Cat('cat1') var c2 = new Cat('cat2') var a1 = new Animal('ani',10) // 驗證子父類原型對象共享問題 Animal.prototype.area = "Asia" Cat.prototype.gender = "formall" console.log(c1.area) // Asia console.log(a1.gender) // undeifined // 驗證沒法訪問父類實例屬性問題 console.log(c1.species) // 動物 // 驗證不一樣實例共享父類實例屬性問題 c1.list.push(4) console.log(c1.list) // [1,2,3,4] console.log(c2.list) // [1,2,3]
其實繼承還有不少種實現方式,就不一一舉例了。並無最好的方式,不一樣的實現有各自的優缺點,找到最適合的就是最好的。