昨天主要介紹了原型,在js中,原型,原型鏈和繼承是三個很重要的概念,而這幾個概念也是面試中經常會被問到的問題,今天,就把昨天還沒總結的原型鏈和繼承繼續做一個整理,希望大家一起學習,一起進步呀O(∩_∩)O 一、原型鏈 學過java的同學應該都知道,繼承是java的重要特點之一,許多面向對象的語言都支 ...
昨天主要介紹了原型,在js中,原型,原型鏈和繼承是三個很重要的概念,而這幾個概念也是面試中經常會被問到的問題,今天,就把昨天還沒總結的原型鏈和繼承繼續做一個整理,希望大家一起學習,一起進步呀O(∩_∩)O
一、原型鏈
學過java的同學應該都知道,繼承是java的重要特點之一,許多面向對象的語言都支持兩種繼承方式:介面繼承和實現繼承,介面繼承只繼承方法簽名,而實現繼承則繼承實際的方法,在js中,由於函數沒有簽名,因此支持實現繼承,而實現繼承主要是依靠原型鏈來實現的,那麼,什麼是原型鏈呢?
首先,我們先來回顧一下構造函數,原型和實例之間的關係
當我們創建一個構造函數時,構造函數會獲得一個prototype屬性,該屬性是一個指針,指向一個原型對象,原型對象包含一個constructor屬性,該屬性也是一個指針,指向構造函數,而當我們創建構造函數的實例時,該實例其實會獲得一個[[Prototype]]屬性,指向原型對象
function SubType() {} var instance = new SubType();
比如上面的代碼,其中,SubType是構造函數,SubType.prototype是原型對象,instance是實例,這三者的關係可以用下麵的圖表示
而這個時候呢,如果我們讓原型對象等於另一個構造函數的實例,此時的原型對象就會獲得一個[[Prototype]]屬性,該屬性會指向另一個原型對象,如果另一個原型對象又是另一個構造函數的實例,這個原型對象又會獲得一個[[Prototype]]屬性,該屬性又會指向另一個原型對象,如此層層遞進,就構成了實例與原型的鏈條,這就是原型鏈
我們再看下上面的例子,如果這個時候,我們讓SubType.prototype是另一個構造函數的實例,此時會怎麼樣呢?
function SuperType() {} function SubType() {} SubType.prototype = new SuperType(); var instance = new SubType();
上面的代碼中,我們先是讓SubType繼承了SuperType,接著創建出SubType的實例instance,因此,instance可以訪問SubType和SuperType原型上的屬性和方法,也就是實現了繼承,繼承關係我們可以用下麵的圖說明
最後,要提醒大家的是,所有引用類型預設都繼承了Object,這個繼承也是通過原型鏈實現的,因此,其實原型鏈的頂層就是Object的原型對象啦
二、繼承
上面我們弄清了原型鏈,接下來主要就介紹一些經常會用到的繼承方法,具體要用哪一種,還是需要依情況而定的
1、原型鏈繼承
最常見的繼承方法就是使用原型鏈實現繼承啦,也就是我們上面所介紹的,接下來,還是看一個實際的例子把
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; } function SubType() { ths.subproperty = true; } SubType.prototype = new SuperType(); // 實現繼承 SubType.prototype.getSubValue = function() { return this.subprototype; } var instance = new SubType(); console.log(instance.getSuperValue()); // true
上面的例子中,我們沒有使用SubType預設提供的原型,而是給它換了一個新原型,這個新原型就是SuperType的實例,因此,新原型具有作為SuperType實例所擁有的全部實現和方法,並且指向SuperType的原型,因此,instance實例具有subproperty屬性,SubType.prototype具有property屬性,值為true,並且擁有getSubValue方法,而SuperType擁有getSuperValue方法
當調用instance的getSuperValue()方法時,因此在instance實例上找不到該方法,就會順著原型鏈先找到SubType.prototype,還是找不到該方法,繼續順著原型鏈找到SuperType.prototype,終於找到getSuperValue,就調用了該函數,而該函數返回property,該值的查找也是同樣的道理,會在SubType.prototype中找到該屬性,值為true,所以顯示true
存在的問題:通過原型鏈實現繼承時,原型實際上會變成另一個類型實例,而原先的實例屬性也會變成原型屬性,如果該屬性為引用類型時,所有的實例都會共用該屬性,一個實例修改了該屬性,其它實例也會發生變化,同時,在創建子類型時,我們也不能向超類型的構造函數中傳遞參數
2、借用構造函數
為瞭解決原型中包含引用類型值所帶來的問題,開發人員開始使用借用構造函數的技術實現繼承,該方法主要是通過apply()和call()方法,在子類型構造函數的內部調用超類型構造函數,從而解決該問題
function SuperType() { this.colors = ["red","blue","green"] } function SubType() { SuperType.call(this); // 實現繼承 } var instance1 = new SubType(); var instance2 = new SubType(); instance2.colors.push("black"); console.log(instance1.colors"); // red,blue,green console.log(instance2.colors"); // red,blue,green,black
在上面的例子中,如果我們使用原型鏈繼承,那麼instance1和instance2將會共用colors屬性,因為colors屬性存在於SubType.prototype中,而上面我們使用了借用構造函數繼承,通過使用call()方法,我們實際上是在新創建的SubType實例的環境下調用了SuperType的構造函數,因此,colors屬性是分別存在instance1和instance2實例中的,修改其中一個不會影響另一個
使用這個方法,我們還可以在子類型構造函數中向超類型構造函數傳遞參數
function SuperType(name) { this.name = name; } function SubType() { SuperType.call(this,"Nicholas"); this.age = 29; } var instance = new SubType(); console.log(instance.name); // Nicholas console.log(instance.age); // 29
優點:解決了原型鏈繼承中引用類型的共用問題,同時可以在子類型構造函數中向超類型構造函數傳遞參數
缺點:定義方法時,將會在每個實例上都會重新定義,不能實現函數的復用
3、組合繼承
組合繼承主要是將原型鏈和借用構造函數的技術組合到一塊,從而發貨兩者之長的一種繼承模式,主要是使用原型鏈實現對原型屬性和方法的基礎,通過借用構造函數實現對實例屬性的基礎,這樣,可以通過在原型上定義方法實現函數的復用,又能夠保證每個實例都有自己的屬性
function SuperType(name) { this.name = name; this.colors = ["red","blue","green"] } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name,age) { SuperType.call(this,name); this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); } var instance1 = new SubType("Nicholas", 29); var instance2 =new SubType("Greg", 27); instance1.colors.push("black"); console.log(instance1.colors); // red,blue,green,black console.log(instance2.colors); // red,blue,green instance1.sayName(); // Nicholas instance2.sayName(); // 29 instance1.sayAge(); // Greg instance2.sayAge(); // 27
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,現在已經成為js中最常用的繼承方法
缺點:無論什麼情況下,都會調用兩次超類型構造函數,一次是在創建子類型的時候,另一次是在子類型構造函數內部,子類型最終會包含超類型對象的全部實例屬性,但是需要在調用子類型構造函數時重寫這些屬性
4、原型式繼承
原型式繼承主要的藉助原型可以基於已有的對象創建新的對象,基本思想就是創建一個臨時性的構造函數,然後將傳入的對象作為這個構造函數的原型,最後返回這個臨時類型的一個新實例
function Object(o) { function F() {} F.prototype = o; return new F(); }
從上面的例子我們可以看出,如果我們想創建一個對象,讓它繼承另一個對象的話,就可以將要被繼承的對象當做o傳遞到Object函數裡面去,Object函數裡面返回的將會是一個新的實例,並且這個實例繼承了o對象
其實,如果我們要使用原型式繼承的話,可以直接通過Object.create()方法來實現,這個方法接收兩個參數,第一個參數是用作新對象原型的對象,第二個參數是一個為新對象定義額外屬性的對象,一般來說,第二個參數可以省略
var person = { name: "Nicholas", friends: ["Shelby","Court","Van"] } var anotherPerson = Object.create(person, { name: { value: "Greg" } }); console.log(anotherPerson.name); // Greg
上面的例子中,我們讓anotherPerson繼承了person,其中,friends作為引用類型,將會被所有繼承該對象的對象所共用,而通過傳入第二個參數,我們可以定義額外的屬性,修改person中的原有信息
缺點:原型式繼承中包含引用類型的屬性始終都會共用相應的值
5、寄生式繼承
寄生式繼承其實和我們前面說的創建對象方法中的寄生構造函數和工程模式很像,創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方法來增強對象,最後再返回該對象
function createAnother(original) { var clone = Object(original); // 通過調用函數創建一個新對象 clone.sayHi = function() { console.log("hi"); } return clone; }
我們其實可以把寄生式繼承看做是傳進去一個對象,然後對該對象進行一定的加工,也就是增加一些方法來增強該對象,然後再返回一個包含新方法的對象的一個過程
var person = { name: "Nicholas", friends:["Shelby","Court","Van"] } var anotherPerson = createAnother(person); anotherPerson.sayHi(); // hi
從上面的代碼中我們可以看出,原來person是沒有包含任何方法的,而通過將person傳進去createAnother方法中進行加工,返回的新對象就包含了一個新的方法
缺點:不能實現函數的復用
6、寄生組合式繼承
組合繼承是js中最經常用到的一種繼承方法,而我們前面也已經說了組合繼承的缺點,組合繼承需要調用兩次超類型構造函數,一次是在創建子類型原型的時候,另一次是在子類型構造函數內部,子類型最終會包含超類型對象的全部實例屬性,但是我們不得不在調用子類型構造函數時重寫這些屬性
function SuperType(name) { this.name = name; this.colors = ["red","blue","green"] } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name,age) { SuperType.call(this,name); // 第二次調用超類型構造函數 this.age = age; } SubType.prototype = new SuperType(); // 第一次調用超類型構造函數 SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); }
上面的代碼中有兩次調用了超類型構造函數,那兩次調用會帶來什麼結果呢?結果就是在SubType.prototype和SubType的實例上都會創建name和colors屬性,最後SubType的實例上的name和colors屬性會屏蔽掉SubType.prototype上的name和colors屬性
寄生組合式繼承就是可以解決上面這個問題,寄生組合式繼承主要通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法,其實就是不必為了指定子類型的原型而調用超類型的構造函數,只需要超類型原型的一個副本就可以了
function inheritPrototype(subType,SuperType) { var prototype = Object(SuperType); // 創建對象 prototype.constructor = subType; // 增強對象 subType.prototype = prototype; // 指定對象 }
在上面的例子中,第一步創建了超類型原型的一個副本,第二步為創建的副本添加constructor屬性,從而彌補因重寫原型而失去的預設的constructor屬性,最後一步將副本也就是新對象賦值給子類型的原型,因此,我們可以用這個函數去替換前面說到為子類型原型賦值的語句
function SuperType(name) { this.name = name; this.colors = ["red","blue","green"] } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name,age) { SuperType.call(this,name); this.age = age; } inheritPrototype(SubType,SuperType); SubType.prototype.sayAge = function() { console.log(this.age); }
寄生組合式繼承只調用了一次SuperType構造函數,避免了在SubType.prototype上面創建的不必要的,多餘的屬性,現在也是很多人使用這種方法實現繼承啦
7、es6中的繼承
我們在前面創建對象中也提到了es6中可以使用Class來創建對象,而同樣的道理,在es6中,也新增加了extends實現Class的繼承,Class 可以通過extends
關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多
class Point {
}
class ColorPoint extends Point {
}
上面這個例子中可以實現ColorPoint類繼承Point類,這種簡潔的語法確實比我們上面介紹的那些方法要簡潔的好多呀
但是呢,使用extends實現繼承的時候,還是有幾點需要註意的問題,子類在繼承父類的時候,子類必須在constructor
方法中調用super
方法,否則新建實例時會報錯,這是因為子類自己的this
對象,必須先通過父類的構造函數完成塑造,得到與父類同樣的實例屬性和方法,然後再對其進行加工,加上子類自己的實例屬性和方法,如果不調用super
方法,子類就得不到this
對象
class Point { constructor(x, y) { this.x = x; this.y = y; } } class ColorPoint extends Point { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); this.color = color; // 正確 } }
上面代碼中,子類的constructor
方法沒有調用super
之前,就使用this
關鍵字,結果報錯,而放在super
方法之後就是正確的,正確的繼承之後,我們就可以創建實例了
let cp = new ColorPoint(25, 8, 'green'); cp instanceof ColorPoint // true cp instanceof Point // true
對於es6的繼承,如果大家想繼續瞭解,可以進行更進一步的學習
今天就介紹到這裡啦,對於js的繼承方法,不知道大家是不是更瞭解了呢