昨天主要介紹了原型,在js中,原型,原型鏈和繼承是三個很重要的概念,而這幾個概念也是面試中常常會被問到的問題,今天,就把昨天還沒總結的原型鏈和繼承繼續作一個整理,但願你們一塊兒學習,一塊兒進步呀O(∩_∩)Ojava
1、原型鏈es6
學過java的同窗應該都知道,繼承是java的重要特色之一,許多面向對象的語言都支持兩種繼承方式:接口繼承和實現繼承,接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法,在js中,因爲函數沒有簽名,所以支持實現繼承,而實現繼承主要是依靠原型鏈來實現的,那麼,什麼是原型鏈呢?面試
首先,咱們先來回顧一下構造函數,原型和實例之間的關係app
當咱們建立一個構造函數時,構造函數會得到一個prototype屬性,該屬性是一個指針,指向一個原型對象,原型對象包含一個constructor屬性,該屬性也是一個指針,指向構造函數,而當咱們建立構造函數的實例時,該實例其實會得到一個[[Prototype]]屬性,指向原型對象函數
function SubType() {} var instance = new SubType();
好比上面的代碼,其中,SubType是構造函數,SubType.prototype是原型對象,instance是實例,這三者的關係能夠用下面的圖表示學習
而這個時候呢,若是咱們讓原型對象等於另外一個構造函數的實例,此時的原型對象就會得到一個[[Prototype]]屬性,該屬性會指向另外一個原型對象,若是另外一個原型對象又是另外一個構造函數的實例,這個原型對象又會得到一個[[Prototype]]屬性,該屬性又會指向另外一個原型對象,如此層層遞進,就構成了實例與原型的鏈條,這就是原型鏈this
咱們再看下上面的例子,若是這個時候,咱們讓SubType.prototype是另外一個構造函數的實例,此時會怎麼樣呢?spa
function SuperType() {} function SubType() {} SubType.prototype = new SuperType(); var instance = new SubType();
上面的代碼中,咱們先是讓SubType繼承了SuperType,接着建立出SubType的實例instance,所以,instance能夠訪問SubType和SuperType原型上的屬性和方法,也就是實現了繼承,繼承關係咱們能夠用下面的圖說明prototype
最後,要提醒你們的是,全部引用類型默認都繼承了Object,這個繼承也是經過原型鏈實現的,所以,其實原型鏈的頂層就是Object的原型對象啦指針
2、繼承
上面咱們弄清了原型鏈,接下來主要就介紹一些常常會用到的繼承方法,具體要用哪種,仍是須要依狀況而定的
一、原型鏈繼承
最多見的繼承方法就是使用原型鏈實現繼承啦,也就是咱們上面所介紹的,接下來,仍是看一個實際的例子把
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
存在的問題:經過原型鏈實現繼承時,原型實際上會變成另外一個類型實例,而原先的實例屬性也會變成原型屬性,若是該屬性爲引用類型時,全部的實例都會共享該屬性,一個實例修改了該屬性,其它實例也會發生變化,同時,在建立子類型時,咱們也不能向超類型的構造函數中傳遞參數
二、借用構造函數
爲了解決原型中包含引用類型值所帶來的問題,開發人員開始使用借用構造函數的技術實現繼承,該方法主要是經過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
優勢:解決了原型鏈繼承中引用類型的共享問題,同時能夠在子類型構造函數中向超類型構造函數傳遞參數
缺點:定義方法時,將會在每一個實例上都會從新定義,不能實現函數的複用
三、組合繼承
組合繼承主要是將原型鏈和借用構造函數的技術組合到一塊,從而發貨二者之長的一種繼承模式,主要是使用原型鏈實現對原型屬性和方法的基礎,經過借用構造函數實現對實例屬性的基礎,這樣,能夠經過在原型上定義方法實現函數的複用,又可以保證每一個實例都有本身的屬性
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中最經常使用的繼承方法
缺點:不管什麼狀況下,都會調用兩次超類型構造函數,一次是在建立子類型的時候,另外一次是在子類型構造函數內部,子類型最終會包含超類型對象的所有實例屬性,可是須要在調用子類型構造函數時重寫這些屬性
四、原型式繼承
原型式繼承主要的藉助原型能夠基於已有的對象建立新的對象,基本思想就是建立一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回這個臨時類型的一個新實例
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中的原有信息
缺點:原型式繼承中包含引用類型的屬性始終都會共享相應的值
五、寄生式繼承
寄生式繼承其實和咱們前面說的建立對象方法中的寄生構造函數和工程模式很像,建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方法來加強對象,最後再返回該對象
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方法中進行加工,返回的新對象就包含了一個新的方法
缺點:不能實現函數的複用
六、寄生組合式繼承
組合繼承是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上面建立的沒必要要的,多餘的屬性,如今也是不少人使用這種方法實現繼承啦
七、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的繼承方法,不知道你們是否是更瞭解了呢