JavaScript中繼承的七種方式對(duì)比總結(jié)
昨天我們聊了原型鏈,知道了JS對(duì)象之間是怎么“攀親戚”的。今天咱們來(lái)聊聊繼承——也就是怎么讓一個(gè)對(duì)象“認(rèn)祖歸宗”,繼承另一個(gè)對(duì)象的屬性和方法。從最原始的手動(dòng)操作,到ES6優(yōu)雅的class語(yǔ)法,這中間有好幾種姿勢(shì),每種都有自己的脾氣。今天一次性給你盤(pán)清楚。
前言
繼承在JS里就像“房產(chǎn)繼承”——你想把老爹的房子傳給孩子,但又不想直接把房子拆了重新蓋。不同的繼承方式,就像是不同的“過(guò)戶(hù)”手段,有的簡(jiǎn)單粗暴,有的精細(xì)巧妙,有的會(huì)留下后遺癥。
今天我們就來(lái)盤(pán)點(diǎn)JS里實(shí)現(xiàn)繼承的七種方式,從最基礎(chǔ)的到最完善的,讓你在面試官問(wèn)“JS繼承有哪些方式”時(shí),能從容應(yīng)對(duì),還能說(shuō)出各自的優(yōu)缺點(diǎn)。
一、原型鏈繼承:最原始的“血脈相連”
這是JS里最基礎(chǔ)的繼承方式,核心就是讓子類(lèi)的原型指向父類(lèi)的實(shí)例。
function Animal() {
this.name = '動(dòng)物';
this.colors = ['黑色', '白色'];
}
Animal.prototype.eat = function() {
console.log('吃東西');
};
function Dog() {}
// 關(guān)鍵一步:讓Dog的原型指向Animal的實(shí)例
Dog.prototype = new Animal();
const dog1 = new Dog();
const dog2 = new Dog();
dog1.colors.push('金色');
console.log(dog2.colors); // ['黑色', '白色', '金色'] —— 哎呀,被改了!
優(yōu)點(diǎn):實(shí)現(xiàn)了方法的繼承,寫(xiě)起來(lái)簡(jiǎn)單。
缺點(diǎn):
- 引用類(lèi)型的屬性會(huì)被所有實(shí)例共享,一個(gè)改了大家都改。
- 無(wú)法向父類(lèi)構(gòu)造函數(shù)傳參。
- 子類(lèi)實(shí)例的構(gòu)造函數(shù)被“篡改”成了Animal。
這種繼承就像家族企業(yè),祖宗留下的財(cái)產(chǎn)(比如房產(chǎn)證)大家共用,一個(gè)孫子把房子賣(mài)了,其他孫子都沒(méi)了。
二、構(gòu)造函數(shù)繼承:借雞生蛋
為了解決引用共享和傳參問(wèn)題,誕生了“借用構(gòu)造函數(shù)”的方式,在子類(lèi)構(gòu)造函數(shù)里調(diào)用父類(lèi)構(gòu)造函數(shù)。
function Animal(name) {
this.name = name;
this.colors = ['黑色', '白色'];
}
Animal.prototype.eat = function() {
console.log('吃東西');
};
function Dog(name) {
Animal.call(this, name); // 借調(diào)父類(lèi)構(gòu)造函數(shù)
}
const dog1 = new Dog('旺財(cái)');
const dog2 = new Dog('來(lái)福');
dog1.colors.push('金色');
console.log(dog1.colors); // ['黑色', '白色', '金色']
console.log(dog2.colors); // ['黑色', '白色'] —— 沒(méi)被影響!
console.log(dog1.eat); // undefined —— 父類(lèi)原型上的方法沒(méi)繼承到
優(yōu)點(diǎn):
- 解決了引用共享問(wèn)題,每個(gè)實(shí)例有自己的屬性副本。
- 可以向父類(lèi)傳遞參數(shù)。
缺點(diǎn):
- 只能繼承父類(lèi)實(shí)例屬性,繼承不到父類(lèi)原型上的方法。
- 方法都在構(gòu)造函數(shù)里定義,每次創(chuàng)建實(shí)例都會(huì)創(chuàng)建新方法,浪費(fèi)內(nèi)存。
這就像“借錢(qián)不借地”,你把老爹的現(xiàn)金拿來(lái)了,但老爹的祖宅(原型上的方法)沒(méi)拿到。
三、組合繼承:取長(zhǎng)補(bǔ)短
把原型鏈繼承和構(gòu)造函數(shù)繼承結(jié)合起來(lái),各取所長(zhǎng)。
function Animal(name) {
this.name = name;
this.colors = ['黑色', '白色'];
}
Animal.prototype.eat = function() {
console.log('吃東西');
};
function Dog(name) {
Animal.call(this, name); // 第二次調(diào)用父類(lèi)
}
Dog.prototype = new Animal(); // 第一次調(diào)用父類(lèi)
Dog.prototype.constructor = Dog; // 修正構(gòu)造函數(shù)指向
const dog1 = new Dog('旺財(cái)');
const dog2 = new Dog('來(lái)福');
dog1.colors.push('金色');
console.log(dog1.colors); // ['黑色', '白色', '金色']
console.log(dog2.colors); // ['黑色', '白色']
dog1.eat(); // 吃東西 —— 方法也繼承到了
優(yōu)點(diǎn):
- 解決了引用共享問(wèn)題。
- 可以向父類(lèi)傳參。
- 繼承了父類(lèi)原型上的方法。
缺點(diǎn):父類(lèi)構(gòu)造函數(shù)被調(diào)用了兩次,造成了一定的性能浪費(fèi)和屬性冗余(實(shí)例上有,原型上也有)。
組合繼承是JS里最常用的繼承方式,雖然有小瑕疵,但足夠好用。ES6的class本質(zhì)上就是它的語(yǔ)法糖。
四、原型式繼承:Object.create的雛形
這種方式不涉及構(gòu)造函數(shù),直接通過(guò)一個(gè)對(duì)象創(chuàng)建另一個(gè)對(duì)象。
const animal = {
name: '動(dòng)物',
colors: ['黑色', '白色'],
eat() {
console.log('吃東西');
}
};
function createObject(obj) {
function F() {}
F.prototype = obj;
return new F();
}
const dog1 = createObject(animal);
const dog2 = createObject(animal);
dog1.colors.push('金色');
console.log(dog2.colors); // ['黑色', '白色', '金色'] —— 又被共享了
ES5提供了Object.create()方法,就是干這個(gè)的。
const dog1 = Object.create(animal); const dog2 = Object.create(animal);
優(yōu)點(diǎn):不需要構(gòu)造函數(shù),直接基于已有對(duì)象創(chuàng)建新對(duì)象。
缺點(diǎn):引用類(lèi)型屬性還是會(huì)被共享。
這種繼承就像“克隆人”,克隆體共享同一個(gè)原型,一個(gè)改了大家都改。
五、寄生式繼承:給繼承加個(gè)包裝
在原型式繼承的基礎(chǔ)上,給新對(duì)象添加方法。
function createDog(original) {
const clone = Object.create(original);
clone.bark = function() {
console.log('汪汪汪');
};
return clone;
}
const animal = { name: '動(dòng)物', eat() { console.log('吃東西'); } };
const dog = createDog(animal);
dog.bark(); // 汪汪汪
優(yōu)點(diǎn):可以在不修改原始對(duì)象的情況下添加新功能。
缺點(diǎn):和原型式繼承一樣,引用共享問(wèn)題依然存在;而且方法每次創(chuàng)建都會(huì)重新生成,沒(méi)法復(fù)用。
六、寄生組合繼承:最完美的姿勢(shì)
組合繼承的缺點(diǎn)是調(diào)用了兩次父類(lèi)構(gòu)造函數(shù)。寄生組合繼承解決了這個(gè)問(wèn)題,它被認(rèn)為是JS繼承的“最佳實(shí)踐”。
function Animal(name) {
this.name = name;
this.colors = ['黑色', '白色'];
}
Animal.prototype.eat = function() {
console.log('吃東西');
};
function Dog(name) {
Animal.call(this, name); // 只調(diào)用一次父類(lèi)
}
// 核心:用Object.create代替new Animal()
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('汪汪汪');
};
const dog1 = new Dog('旺財(cái)');
const dog2 = new Dog('來(lái)福');
dog1.colors.push('金色');
console.log(dog2.colors); // ['黑色', '白色'] —— 不受影響
dog1.eat(); // 吃東西
dog1.bark(); // 汪汪汪
優(yōu)點(diǎn):
- 父類(lèi)構(gòu)造函數(shù)只調(diào)用一次。
- 原型鏈干干凈凈,沒(méi)有冗余屬性。
- 既有自己的屬性副本,又繼承了原型方法。
寄生組合繼承是目前最理想的繼承實(shí)現(xiàn)方式,也是ES6 class 背后做的事情。
七、ES6 Class:語(yǔ)法糖的終極形態(tài)
ES6引入了class關(guān)鍵字,讓繼承寫(xiě)起來(lái)像其他語(yǔ)言一樣優(yōu)雅。
class Animal {
constructor(name) {
this.name = name;
this.colors = ['黑色', '白色'];
}
eat() {
console.log('吃東西');
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 調(diào)用父類(lèi)構(gòu)造函數(shù)
this.breed = breed;
}
bark() {
console.log('汪汪汪');
}
}
const dog1 = new Dog('旺財(cái)', '土狗');
const dog2 = new Dog('來(lái)福', '金毛');
dog1.colors.push('金色');
console.log(dog2.colors); // ['黑色', '白色'] —— 完美
dog1.eat(); // 吃東西
dog1.bark(); // 汪汪汪
優(yōu)點(diǎn):
- 語(yǔ)法清晰,易讀易寫(xiě)。
- 底層就是寄生組合繼承,性能好。
- 支持
super關(guān)鍵字方便調(diào)用父類(lèi)方法。
缺點(diǎn):本質(zhì)還是原型那一套,但語(yǔ)法糖已經(jīng)夠甜了。
八、各種繼承方式對(duì)比總結(jié)
| 繼承方式 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|
| 原型鏈繼承 | 簡(jiǎn)單 | 引用共享、不能傳參 | 基本不用 |
| 構(gòu)造函數(shù)繼承 | 解決引用共享、能傳參 | 不能繼承原型方法 | 基本不用 |
| 組合繼承 | 兩者優(yōu)點(diǎn)都有 | 調(diào)用兩次父類(lèi) | 以前常用 |
| 原型式繼承 | 基于已有對(duì)象創(chuàng)建 | 引用共享 | 簡(jiǎn)單對(duì)象復(fù)用 |
| 寄生式繼承 | 可添加新功能 | 引用共享 | 臨時(shí)增強(qiáng)對(duì)象 |
| 寄生組合繼承 | 完美 | 寫(xiě)法稍復(fù)雜 | ES6之前的首選 |
| ES6 class | 語(yǔ)法優(yōu)雅、標(biāo)準(zhǔn) | 需要轉(zhuǎn)譯(老環(huán)境) | 現(xiàn)代開(kāi)發(fā)首選 |
九、實(shí)際開(kāi)發(fā)中怎么選
無(wú)腦選ES6 class。除非你還要兼容IE這種古董,否則直接用class和extends就完事了。不僅代碼量少,而且不容易踩坑。
如果你好奇class底層干了啥,或者要寫(xiě)一些高階的繼承場(chǎng)景(比如混入multiple inheritance),那寄生組合繼承的手寫(xiě)實(shí)現(xiàn)還是值得掌握的。
function inherit(child, parent) {
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
這個(gè)工具函數(shù),就是寄生組合繼承的核心。
十、總結(jié):從“手動(dòng)擋”到“自動(dòng)擋”
JS的繼承演進(jìn)史,其實(shí)就是一部從“手動(dòng)擋”到“自動(dòng)擋”的發(fā)展史:
- 原型鏈繼承是手動(dòng)擋,操作復(fù)雜容易出事。
- 組合繼承是自動(dòng)擋,但油耗(性能)稍高。
- 寄生組合繼承是CVT,平順又高效。
- ES6 class是智能駕駛,你只管踩油門(mén),剩下的交給它。
無(wú)論哪種方式,底層都是原型鏈那一套。掌握了原型,繼承的七種姿勢(shì)不過(guò)是排列組合。以后面試官再問(wèn)“JS繼承有哪些方式”,你可以從容地把這七種娓娓道來(lái),順便告訴他:“但實(shí)際開(kāi)發(fā),我選ES6 class。”
明天我們將進(jìn)入JavaScript的另一個(gè)核心領(lǐng)域——異步編程,從回調(diào)地獄到Promise,再到async/await,帶你徹底理清JS的異步世界。
以上就是JavaScript中繼承的七種方式對(duì)比總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于JavaScript繼承的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript實(shí)現(xiàn)通過(guò)滑塊改變網(wǎng)頁(yè)顏色
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)通過(guò)滑塊改變網(wǎng)頁(yè)顏色,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Bootstrap的Carousel配合dropload.js實(shí)現(xiàn)移動(dòng)端滑動(dòng)切換圖片
這篇文章主要介紹了bootstrap的Carousel配合dropload.js實(shí)現(xiàn)移動(dòng)端滑動(dòng)切換圖片,實(shí)現(xiàn)方法非常簡(jiǎn)單,具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03
javascript 文字上下間隔滾動(dòng)的代碼 符合WEB標(biāo)準(zhǔn) 腳本之家修正版
javascript 文字上下間隔滾動(dòng)的代碼 符合WEB標(biāo)準(zhǔn) 腳本之家修正版,這里提供了兩個(gè)版本,第二個(gè)在firefox下運(yùn)行有些問(wèn)題大家可以修改下,第一個(gè)的高度問(wèn)題,已經(jīng)修正,其實(shí)就是簡(jiǎn)單的加了css樣式。2009-12-12
非主流的textarea自增長(zhǎng)實(shí)現(xiàn)js代碼
今天稍微研究了下textarea隨輸入內(nèi)容自動(dòng)增長(zhǎng)的功能,通過(guò)google參考了一些實(shí)現(xiàn)方式2011-12-12
JavaScript實(shí)現(xiàn)無(wú)縫輪播圖的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用JavaScript語(yǔ)言實(shí)現(xiàn)無(wú)縫輪播功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
微信公眾號(hào)weixin-js-sdk使用方法總結(jié)
最近做了一個(gè)活動(dòng)頁(yè)面,需要自定義微信分享的標(biāo)題、詳情、縮略圖和url,使用到了jssdk,這篇文章主要給大家介紹了關(guān)于微信公眾號(hào)weixin-js-sdk使用方法的相關(guān)資料,需要的朋友可以參考下2022-12-12
JavaScript本地?cái)?shù)據(jù)存儲(chǔ)sessionStorage與localStorage使用詳解
這篇文章主要介紹了JavaScript本地?cái)?shù)據(jù)存儲(chǔ)sessionStorage與localStorage使用,localStorage:永久存儲(chǔ)在本地,適合保存在本地的數(shù)據(jù)。sessionStorage:會(huì)話(huà)級(jí)的存儲(chǔ),敏感帳號(hào)一次登陸2022-10-10
Java中int與integer的區(qū)別(基本數(shù)據(jù)類(lèi)型與引用數(shù)據(jù)類(lèi)型)
這篇文章主要介紹了int與integer的區(qū)別(基本數(shù)據(jù)類(lèi)型與引用數(shù)據(jù)類(lèi)型),簡(jiǎn)單的說(shuō) int 是基本數(shù)據(jù)類(lèi)型,integer 是引用數(shù)據(jù)類(lèi)型,具體區(qū)別詳解大家參考下本文2017-02-02

