一、原型鏈
學(xué)過java的同學(xué)應(yīng)該都知道,繼承是java的重要特點之一,許多面向?qū)ο蟮恼Z言都支持兩種繼承方式:接口繼承和實現(xiàn)繼承椎木,接口繼承只繼承方法簽名,而實現(xiàn)繼承則繼承實際的方法博烂,在js中香椎,由于函數(shù)沒有簽名,因此支持實現(xiàn)繼承禽篱,而實現(xiàn)繼承主要是依靠原型鏈來實現(xiàn)的畜伐,那么,什么是原型鏈呢躺率?
首先玛界,我們先來回顧一下構(gòu)造函數(shù)万矾,原型和實例之間的關(guān)系。當(dāng)我們創(chuàng)建一個構(gòu)造函數(shù)時慎框,構(gòu)造函數(shù)會獲得一個prototype
屬性良狈,該屬性是一個指針,指向一個原型對象笨枯,原型對象包含一個constructor
屬性薪丁,該屬性也是一個指針,指向構(gòu)造函數(shù)馅精,而當(dāng)我們創(chuàng)建構(gòu)造函數(shù)的實例時严嗜,該實例其實會獲得一個[[Prototype]]
屬性,指向原型對象洲敢。
function SubType() {}
var instance = new SubType();
比如上面的代碼漫玄,其中,SubType是構(gòu)造函數(shù)压彭,SubType.prototype是原型對象睦优,instance是實例,這三者的關(guān)系可以用下面的圖表示壮不。
而這個時候呢刨秆,如果我們讓原型對象等于另一個構(gòu)造函數(shù)的實例,此時的原型對象就會獲得一個[[Prototype]]屬性忆畅,該屬性會指向另一個原型對象,如果另一個原型對象又是另一個構(gòu)造函數(shù)的實例尸执,這個原型對象又會獲得一個[[Prototype]]屬性家凯,該屬性又會指向另一個原型對象,如此層層遞進如失,就構(gòu)成了實例與原型的鏈條绊诲,這就是原型鏈。
我們再看下上面的例子褪贵,如果這個時候掂之,我們讓SubType.prototype是另一個構(gòu)造函數(shù)的實例,此時會怎么樣呢脆丁?
function SuperType() {}
function SubType() {}
SubType.prototype = new SuperType();
var instance = new SubType();
上面的代碼中世舰,我們先是讓SubType繼承了SuperType,接著創(chuàng)建出SubType的實例instance槽卫,因此跟压,instance可以訪問SubType和SuperType原型上的屬性和方法,也就是實現(xiàn)了繼承歼培,繼承關(guān)系我們可以用下面的圖說明震蒋。
最后茸塞,要提醒大家的是,所有引用類型默認都繼承了Object查剖,這個繼承也是通過原型鏈實現(xiàn)的钾虐,因此,其實原型鏈的頂層就是Object的原型對象啦笋庄。
二效扫、繼承
上面我們弄清了原型鏈,接下來主要就介紹一些經(jīng)常會用到的繼承方法无切,具體要用哪一種荡短,還是需要依情況而定的。
1. 原型鏈繼承
最常見的繼承方法就是使用原型鏈實現(xiàn)繼承啦哆键,也就是我們上面所介紹的掘托,接下來,還是看一個實際的例子籍嘹。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
ths.subproperty = true;
}
SubType.prototype = new SuperType();// 實現(xiàn)繼承
SubType.prototype.getSubValue = function() {
return this.subprototype;
}
var instance = new SubType();
console.log(instance.getSuperValue());// true
上面的例子中闪盔,我們沒有使用SubType默認提供的原型,而是給它換了一個新原型辱士,這個新原型就是SuperType的實例泪掀,因此,新原型具有作為SuperType實例所擁有的全部實現(xiàn)和方法颂碘,并且指向SuperType的原型异赫,因此,instance實例具有subproperty屬性头岔,SubType.prototype具有property屬性塔拳,值為true,并且擁有g(shù)etSubValue方法峡竣,而SuperType擁有g(shù)etSuperValue方法靠抑。
當(dāng)調(diào)用instance的getSuperValue()方法時,因此在instance實例上找不到該方法适掰,就會順著原型鏈先找到SubType.prototype颂碧,還是找不到該方法,繼續(xù)順著原型鏈找到SuperType.prototype类浪,終于找到getSuperValue载城,就調(diào)用了該函數(shù),而該函數(shù)返回property费就,該值的查找也是同樣的道理个曙,會在SubType.prototype中找到該屬性,值為true,所以顯示true垦搬。
存在的問題:通過原型鏈實現(xiàn)繼承時呼寸,原型實際上會變成另一個類型實例,而原先的實例屬性也會變成原型屬性猴贰,如果該屬性為引用類型時对雪,所有的實例都會共享該屬性,一個實例修改了該屬性米绕,其它實例也會發(fā)生變化瑟捣,同時,在創(chuàng)建子類型時栅干,我們也不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)迈套。
2. 借用構(gòu)造函數(shù)
為了解決原型中包含引用類型值所帶來的問題,開發(fā)人員開始使用借用構(gòu)造函數(shù)的技術(shù)實現(xiàn)繼承碱鳞,該方法主要是通過apply()和call()方法桑李,在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù),從而解決該問題窿给。
function SuperType() {
this.colors = ["red","blue","green"]
}
function SubType() {
SuperType.call(this);// 實現(xiàn)繼承
}
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中禁荒,而上面我們使用了借用構(gòu)造函數(shù)繼承,通過使用call()方法角撞,我們實際上是在新創(chuàng)建的SubType實例的環(huán)境下調(diào)用了SuperType的構(gòu)造函數(shù)呛伴,因此,colors屬性是分別存在instance1和instance2實例中的谒所,修改其中一個不會影響另一個磷蜀。
使用這個方法,我們還可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)百炬。
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
優(yōu)點:解決了原型鏈繼承中引用類型的共享問題,同時可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)污它。
缺點:定義方法時剖踊,將會在每個實例上都會重新定義,不能實現(xiàn)函數(shù)的復(fù)用衫贬。
3. 組合繼承
組合繼承主要是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊德澈,從而發(fā)貨兩者之長的一種繼承模式,主要是使用原型鏈實現(xiàn)對原型屬性和方法的基礎(chǔ)固惯,通過借用構(gòu)造函數(shù)實現(xiàn)對實例屬性的基礎(chǔ)梆造,這樣,可以通過在原型上定義方法實現(xiàn)函數(shù)的復(fù)用,又能夠保證每個實例都有自己的屬性镇辉。
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
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷屡穗,融合了它們的優(yōu)點,現(xiàn)在已經(jīng)成為js中最常用的繼承方法忽肛。
缺點:無論什么情況下村砂,都會調(diào)用兩次超類型構(gòu)造函數(shù),一次是在創(chuàng)建子類型的時候屹逛,另一次是在子類型構(gòu)造函數(shù)內(nèi)部础废,子類型最終會包含超類型對象的全部實例屬性,但是需要在調(diào)用子類型構(gòu)造函數(shù)時重寫這些屬性罕模。
4. 原型式繼承
原型式繼承主要的借助原型可以基于已有的對象創(chuàng)建新的對象评腺,基本思想就是創(chuàng)建一個臨時性的構(gòu)造函數(shù),然后將傳入的對象作為這個構(gòu)造函數(shù)的原型淑掌,最后返回這個臨時類型的一個新實例蒿讥。
function Object(o) {
function F() {}
F.prototype = o;
return new F();
}
從上面的例子我們可以看出,如果我們想創(chuàng)建一個對象锋拖,讓它繼承另一個對象的話诈悍,就可以將要被繼承的對象當(dāng)做o傳遞到Object函數(shù)里面去,Object函數(shù)里面返回的將會是一個新的實例兽埃,并且這個實例繼承了o對象侥钳。
其實,如果我們要使用原型式繼承的話柄错,可以直接通過Object.create()方法來實現(xiàn)芙粱,這個方法接收兩個參數(shù),第一個參數(shù)是用作新對象原型的對象伍宦,第二個參數(shù)是一個為新對象定義額外屬性的對象丙号,一般來說,第二個參數(shù)可以省略颂跨。
var person = {
name: "Nicholas",
friends: ["Shelby","Court","Van"]
}
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson.name); // Greg
上面的例子中敢伸,我們讓anotherPerson繼承了person,其中恒削,friends作為引用類型池颈,將會被所有繼承該對象的對象所共享,而通過傳入第二個參數(shù)钓丰,我們可以定義額外的屬性躯砰,修改person中的原有信息。
缺點:原型式繼承中包含引用類型的屬性始終都會共享相應(yīng)的值携丁。
5. 寄生式繼承
寄生式繼承其實和我們前面說的創(chuàng)建對象方法中的寄生構(gòu)造函數(shù)和工程模式很像琢歇,創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方法來增強對象,最后再返回該對象李茫。
function createAnother(original) {
var clone = Object(original);
// 通過調(diào)用函數(shù)創(chuàng)建一個新對象
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方法中進行加工塔次,返回的新對象就包含了一個新的方法。
缺點:不能實現(xiàn)函數(shù)的復(fù)用名秀。
6. 寄生組合式繼承
組合繼承是js中最經(jīng)常用到的一種繼承方法励负,而我們前面也已經(jīng)說了組合繼承的缺點,組合繼承需要調(diào)用兩次超類型構(gòu)造函數(shù)匕得,一次是在創(chuàng)建子類型原型的時候继榆,另一次是在子類型構(gòu)造函數(shù)內(nèi)部,子類型最終會包含超類型對象的全部實例屬性汁掠,但是我們不得不在調(diào)用子類型構(gòu)造函數(shù)時重寫這些屬性略吨。
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); // 第二次調(diào)用超類型構(gòu)造函數(shù)
this.age = age;
}
SubType.prototype = new SuperType(); // 第一次調(diào)用超類型構(gòu)造函數(shù)
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}
上面的代碼中有兩次調(diào)用了超類型構(gòu)造函數(shù),那兩次調(diào)用會帶來什么結(jié)果呢考阱?結(jié)果就是在SubType.prototype和SubType的實例上都會創(chuàng)建name和colors屬性翠忠,最后SubType的實例上的name和colors屬性會屏蔽掉SubType.prototype上的name和colors屬性。
寄生組合式繼承就是可以解決上面這個問題乞榨,寄生組合式繼承主要通過借用構(gòu)造函數(shù)來繼承屬性秽之,通過原型鏈的混成形式來繼承方法,其實就是不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù)吃既,只需要超類型原型的一個副本就可以了考榨。
function inheritPrototype(subType,SuperType) {
var prototype = Object(SuperType); // 創(chuàng)建對象
prototype.constructor = subType; // 增強對象
subType.prototype = prototype; // 指定對象
}
在上面的例子中,第一步創(chuàng)建了超類型原型的一個副本鹦倚,第二步為創(chuàng)建的副本添加constructor屬性河质,從而彌補因重寫原型而失去的默認的constructor屬性,最后一步將副本也就是新對象賦值給子類型的原型震叙,因此掀鹅,我們可以用這個函數(shù)去替換前面說到為子類型原型賦值的語句。
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);
}
寄生組合式繼承只調(diào)用了一次SuperType構(gòu)造函數(shù)捐友,避免了在SubType.prototype上面創(chuàng)建的不必要的,多余的屬性溃槐,現(xiàn)在也是很多人使用這種方法實現(xiàn)繼承啦匣砖。
7. es6中的繼承
我們在前面創(chuàng)建對象中也提到了es6中可以使用Class來創(chuàng)建對象,而同樣的道理,在es6中猴鲫,也新增加了extends實現(xiàn)Class的繼承对人,Class 可以通過extends
關(guān)鍵字實現(xiàn)繼承,這比 ES5 的通過修改原型鏈實現(xiàn)繼承拂共,要清晰和方便很多牺弄。
class Point {}
class ColorPoint extends Point {}
上面這個例子中可以實現(xiàn)ColorPoint類繼承Point類,這種簡潔的語法確實比我們上面介紹的那些方法要簡潔的好多呀宜狐。
但是呢势告,使用extends實現(xiàn)繼承的時候,還是有幾點需要注意的問題抚恒,子類在繼承父類的時候咱台,子類必須在constructor
方法中調(diào)用super
方法,否則新建實例時會報錯俭驮,這是因為子類自己的this
對象回溺,必須先通過父類的構(gòu)造函數(shù)完成塑造,得到與父類同樣的實例屬性和方法混萝,然后再對其進行加工遗遵,加上子類自己的實例屬性和方法,如果不調(diào)用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
方法沒有調(diào)用super
之前厘熟,就使用this
關(guān)鍵字屯蹦,結(jié)果報錯,而放在super
方法之后就是正確的绳姨,正確的繼承之后登澜,我們就可以創(chuàng)建實例了。
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint; // true
cp instanceof Point; // true
看完文章飘庄,還有福利拿哦脑蠕,往下看??????
感興趣的小伙伴可以在公號【grain先森】后臺回復(fù)【190315】獲取【CSS 參考規(guī)范】,可以轉(zhuǎn)發(fā)朋友圈和你的朋友分享哦跪削。