分享JavaScript?中的幾種繼承方式
前言:
說到JavaScript中的繼承,與之密切相關(guān)的就是原型鏈了,JavaScript中的繼承主要是通過原型鏈實(shí)現(xiàn)的。但是簡單的原型鏈繼承方式也存在一定的缺陷,在此借著《JavaScript高級程序設(shè)計(第四版)》一書,聊聊JavaScript中的幾種繼承方式
一、原型鏈
ECMA-262 把原型鏈定義為ECMAScript的主要繼承方式,其基本思想就是通過原型繼承多個引用類型的屬性和方法。
在此回顧一下原型、構(gòu)造函數(shù)、實(shí)例之間的關(guān)系:
每個構(gòu)造函數(shù)都有一個原型對象,原型有一個屬性指回構(gòu)造函數(shù),實(shí)例有一個內(nèi)部指針指向原型。
有關(guān)原型和原型鏈的知識這里先不多說了,這里來談?wù)勗玩湹囊恍﹩栴}。
1.1 原型鏈的問題
- 原型鏈主要問題出現(xiàn)在原型中包含引用值的時候。因?yàn)樵蜕系膶傩詴谒袑傩灾g共享,對于原型上的引用值,實(shí)例繼承的是指向該對象的引用,所以在實(shí)例中修改該屬性時,會影響原型上的屬性。
function Father() {
this.colors = ['red'];
}
function Son() {}
Son.prototype = new Father();
let son1 = new Son();
console.log(son1.colors); // ['red']
son1.colors.push('green');
console.log(son1.colors); // ['red', 'green']
console.log(son1.hasOwnProperty('colors')); // false
let son2 = new Son();
console.log(son2.colors); // ['red', 'green']
console.log(Son.prototype.colors); // ['red', 'green']如上代碼,構(gòu)造函數(shù)的原型為new Father(),原型包含引用值屬性colors。Son對象實(shí)例自身并沒有colors屬性,而是繼承自原型,所以向colors中添加"green"影響到的原型上的colors。這就導(dǎo)致son2訪問colors屬性時值為['red', 'green']。
所以,若原型上屬性為引用值時,在實(shí)例中對該屬性修改時會影響原型屬性。
但是需要注意下面這種情況:
function Father() {
this.colors = ['red'];
}
function Son() {}
Son.prototype = new Father();
let son1 = new Son();
console.log(son1.colors); // ['red']
son1.colors = [];
console.log(son1.colors); // []
console.log(son1.hasOwnProperty('colors')); // true
let son2 = new Son();
console.log(son2.colors); // ['red']
console.log(Son.prototype.colors); // ['red']代碼中son1.colors = []并不是修改原型屬性colors為[],而是在為實(shí)例son1添加新的屬性colors。
- 原型鏈的另一個問題是,子類型在實(shí)例化時不能給父類型的構(gòu)造函數(shù)傳參。即不能在不影響其他對象實(shí)例的情況下傳遞參數(shù)給父類的構(gòu)造函數(shù)。
那上面的代碼來說就是,在創(chuàng)建Son對象實(shí)例的時候,不能指定colors的值。
綜上所述:由于引用值和傳參問題,原型鏈一般不會被單獨(dú)使用。
二、盜用構(gòu)造函數(shù)
為了解決原型包含引用值所導(dǎo)致的問題,出現(xiàn)了一種叫作"盜用構(gòu)造函數(shù)"(constructor stealing)的技術(shù)。
2.1 基本思想
在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)。主要是通過
call和apply來實(shí)現(xiàn)。
function Father() {
this.colors = ['red'];
}
function Son() {
// 在此通過call()調(diào)用父類構(gòu)造函數(shù)
Father.call(this);
}
let son1 = new Son();
console.log(son1.colors); // ['red']
// 說明colors 是實(shí)例的自身屬性
console.log(son1.hasOwnProperty('colors')); // true
son1.colors.push('green');
console.log(son1.colors); // ['red', 'green']
let son2 = new Son();
console.log(son2.colors); // ['red']由new運(yùn)算符調(diào)用構(gòu)造函數(shù)的過程可知,會將函數(shù)中的this指向新創(chuàng)建的實(shí)例。所以Father.call(this);相當(dāng)于實(shí)例調(diào)用了Father方法,然后添加了自身屬性colors。所以后續(xù)son1.colors.push('green');并不會影響到其他實(shí)例。
2.2 可向父類構(gòu)造函數(shù)傳參
盜用構(gòu)造函數(shù)的另外一個優(yōu)點(diǎn)在于,可以在子類構(gòu)造函數(shù)中向父類構(gòu)造函數(shù)傳參。
如下代碼:
function Father(name) {
this.name = name;
}
function Son(name) {
Father.call(this, name);
}
let son = new Son('dali');
console.log(son); // Son?{name: 'dali'}2.3 盜用構(gòu)造函數(shù)的問題
盜用構(gòu)造函數(shù)的主要問題如下:
- 所有方法必須在構(gòu)造函數(shù)中定義,所以方法不能重用。(即:即使功能相同的方法,每個實(shí)例上對應(yīng)的該方法不是同一個函數(shù)對象)
function Father() {
this.foo = function() {}
}
function Son() {
Father.call(this);
}
let son1 = new Son();
let son2 = new Son();
console.log(son1.foo === son2.foo); // false- 子類不能訪問到父類原型上的方法。因?yàn)樽宇悆H僅只是調(diào)用父類構(gòu)造函數(shù),并沒有設(shè)置原型指向父類實(shí)例。子類和父類之間并沒有建立原型關(guān)系。
let son = new Son(); console.log(son instanceof Father) // false
綜上所述:單獨(dú)使用盜用構(gòu)造函數(shù)也是不可行的。
三、組合繼承(偽經(jīng)典繼承)
3.1 基本思想
組合繼承綜合了原型鏈和盜用構(gòu)造函數(shù),使用原型鏈繼承原型上的屬性和方法,通過盜用構(gòu)造函數(shù)繼承實(shí)例屬性。
function Father(name) {
this.name = name;
this.colors = ['red'];
}
Father.prototype.sayHello = function() {
console.log('hello');
}
function Son(name) {
// 繼承屬性
Father.call(this, name);
}
// 構(gòu)建原型鏈,繼承方法
Son.prototype = new Father();
let son1 = new Son('dali');
console.log(son1); // {name: 'dali', ['red']}
son1.colors.push('green');
console.log(son1); // {name: 'dali', ['red', 'green']}
let son2 = new Son('haha');
console.log(son2); // {name: 'haha', ['red']}
// 每個實(shí)例都有自身的 colors 屬性
console.log(son1.colors === son2.colors) // false
// 實(shí)例間共享sayHello方法
console.log(son1.sayHello === son2.sayHello) // true通過調(diào)用父類構(gòu)造函數(shù),每個實(shí)例都有“自身”的原型屬性(例如:colors),所以通過引用修改對應(yīng)的對象時,不會影響其他實(shí)例,因?yàn)槊總€實(shí)例的引用值屬性指向的對象不同。此外,通過原型鏈也實(shí)現(xiàn)了所以實(shí)例之間共享同一方法。
3.2 組合繼承的問題
雖然組合繼承彌補(bǔ)了原型鏈和盜用構(gòu)造函數(shù)的不足,但是組合繼承也存在效率問題:
- 父類的構(gòu)造函數(shù)會被調(diào)用兩次
- 一次時在創(chuàng)建子類原型的時候被調(diào)用
- 另一次是實(shí)例化子類對象時在子類構(gòu)造函數(shù)中被調(diào)用
- 子類原型上存在不必要的屬性
console.log(Son.prototype); // Father?{name: undefined, colors: Array(1)}緊接著上述代碼,我們可以看到子類構(gòu)造函數(shù)的原型對象上有name和colors屬性,但是每個Son對象實(shí)例上都有自身的name和colors屬性,并不是繼承自原型。所以,子類構(gòu)造函數(shù)的原型對象上有name和colors屬性是多余的。
- 子類構(gòu)造函數(shù)原型(prototype)上的
constructor屬性丟失
console.log(Son.prototype.constructor === Son) // false
修改構(gòu)造函數(shù)的原型都會出現(xiàn)這種問題。
四、原型式繼承
4.1 基本思想
function object(o) {
function F();
F.prototype = o;
return new F();
}其實(shí)就是在創(chuàng)建一個對象時,指定該對象的原型。
4.2 Object.create()
在ECMAScript 5 中增加了Object.create()方法,對原型式繼承進(jìn)行了規(guī)范化
Object.create()方法創(chuàng)建一個新對象,使用現(xiàn)有的對象來提供新創(chuàng)建的對象的__proto__。
(1)語法
Object.create(proto,[propertiesObject])
proto: 新創(chuàng)建對象的原型對象。propertiesObject: 可選。需要傳入一個對象,該對象的屬性類型參照Object.defineProperties()的第二個參數(shù)。如果該參數(shù)被指定且不為undefined,該傳入對象的自有可枚舉屬性(即其自身定義的屬性,而不是其原型鏈上的枚舉屬性)將為新創(chuàng)建的對象添加指定的屬性值和對應(yīng)的屬性描述符。- 返回值:一個新對象,帶著指定的原型對象和屬性。
(2)示例
o = Object.create(Object.prototype, {
// foo會成為所創(chuàng)建對象的數(shù)據(jù)屬性
foo: {
writable:true,
configurable:true,
value: "hello"
},
// bar會成為所創(chuàng)建對象的訪問器屬性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
});(3)手動實(shí)現(xiàn)
function objectCreate(proto, propertiesObject=undefined){
// 構(gòu)造函數(shù)
function F() {
}
// 構(gòu)造函數(shù)原型 prototype 鏈接到proto對象
F.prototype = proto;
// 創(chuàng)建對象
const obj = new F();
// 若參數(shù) propertiesObject 被指定且不為 undefined
if (propertiesObject !== undefined) {
// 新創(chuàng)建的對象添加指定的屬性值和對應(yīng)的屬性描述符。
Object.defineProperties(obj, propertiesObject);
}
return obj;
}五、寄生式繼承
5.1 基本思想
創(chuàng)建一個實(shí)現(xiàn)繼承的函數(shù),以某種方式增強(qiáng)對象,然后返回這個對象。
function createAnother(original) {
// 通過調(diào)用函數(shù)創(chuàng)建一個新對象
let clone = Object(original);
// 以某種方式增強(qiáng)這個對象
clone.sayHi = function() {
console.log('hi');
};
// 返回增強(qiáng)的對象
return clone;
}個人理解:寄生式繼承就是通過一個函數(shù),以當(dāng)前對象為基礎(chǔ),創(chuàng)建一個新的對象,并為新的對象添加新的方法。
let obj = {};
let anotherObj = createAnother(obj);
anotherObj.sayHi(); // hi5.2 寄生式繼承
與盜用構(gòu)造函數(shù)類似,寄生式繼承中給對象新增的函數(shù)不能被重用。
六、寄生式組合繼承
針對第三節(jié)中組合繼承存在的問題,可以通過寄生式組合繼承來解決。
6.1 基本思想
不通過調(diào)用父類構(gòu)造函數(shù)給子類原型賦值,而是得到父類原型的一個副本。即使用寄生式繼承來繼承父類原型,然后將返回的新對象賦值給子類原型。
function inheritPrototype(subType, SuperType) {
// 創(chuàng)建對象
let prototype = Object(SuperType.prototype);
// 增強(qiáng)對象(防止修改原型導(dǎo)致constructor丟失)
prototype.constructor = subType;
// 賦值對象
subType.prototype = prototype
}subType:子類構(gòu)造函數(shù)SuperType:父類構(gòu)造函數(shù)
如上代碼:
- 首先創(chuàng)建一個父類原型的副本
- 在副本上添加
constructor屬性,防止在修改原型時丟失了constructor屬性 - 最后修改子類構(gòu)造函數(shù)的原型,實(shí)現(xiàn)繼承
function Father(name) {
this.name = name;
this.colors = ['red'];
console.log('父類構(gòu)造函數(shù)調(diào)用了');
}
Father.prototype.sayHello = function() {
console.log('hello');
}
function Son(name) {
// 繼承屬性
Father.call(this, name);
}
// 寄生式繼承原型
inheritPrototype(Son, Father)
// 父類構(gòu)造函數(shù)只在實(shí)例化時調(diào)用一次
let son = new Son('dali'); // 父類構(gòu)造函數(shù)調(diào)用了
// 子類構(gòu)造函數(shù)中不存在不必要的屬性
console.log(Son.prototype) // {sayHello: ?, constructor: ?}
// 子類構(gòu)造函數(shù)的 constructor 屬性未丟失
console.log(Son.prototype.constructor === Son) // true如上代碼,寄生式組合繼承解決了組合繼承存在的一些問題。綜上,寄生式組合繼承可以算是引用類型繼承的最佳模式。
但是,關(guān)于寄生式組合需要注意的一點(diǎn)是:寄生式繼承函數(shù)在創(chuàng)建對象副本時,如果使用的是Object()函數(shù),對于Object()函數(shù)如果給定值是一個已經(jīng)存在的對象,則會返回這個已經(jīng)存在的值(相同地址)。所以函數(shù)中prototype.constructor = subType;會修改父類原型上的constructor屬性。
console.log(Father.prototype.constructor) // ? Son(name) {// 繼承屬性 Father.call(this, name);}
console.log(Father.prototype.constructor === Father) // false但是,這并不會影響父類對象實(shí)例的創(chuàng)建
console.log(new Father('haha')) // Father?{name: 'haha', colors: Array(1)}到此這篇關(guān)于分享JavaScript 中的幾種繼承方式的文章就介紹到這了,更多相關(guān)JS繼承方式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript html5 canvas實(shí)現(xiàn)圖片上畫超鏈接
這篇文章主要為大家詳細(xì)介紹了JavaScript html5 canvas實(shí)現(xiàn)圖片上畫超鏈接,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10
JavaScript編程設(shè)計模式之構(gòu)造器模式實(shí)例分析
這篇文章主要介紹了JavaScript編程設(shè)計模式之構(gòu)造器模式,簡單講述了構(gòu)造器模式的概念、原理,并結(jié)合實(shí)例形式分析了構(gòu)造器模式的定義與使用方法,需要的朋友可以參考下2017-10-10
Javascript的構(gòu)造函數(shù)和constructor屬性
我們知道,默認(rèn)情況下,對一個函數(shù)前面使用new,可以構(gòu)造出一個對象。每一個對象都有一個constructor屬性,這個constructor屬性指向構(gòu)造出該對象的函數(shù)。2010-01-01
JavaScript canvas實(shí)現(xiàn)九宮格切圖效果
這篇文章主要為大家詳細(xì)介紹了JavaScript canvas實(shí)現(xiàn)九宮格切圖效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09
JavaScript常考手寫題之柯里化與數(shù)組扁平化的實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了JavaScript??际謱戭}中柯里化與數(shù)組扁平化、數(shù)組去重的實(shí)現(xiàn),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12
JavaScript實(shí)現(xiàn)2種常見的抽獎效果實(shí)例代碼
抽獎系統(tǒng)是一種常見的功能,可以用于各種活動和網(wǎng)站,這篇文章主要給大家介紹了關(guān)于JavaScript實(shí)現(xiàn)2種常見的抽獎效果的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01
JS運(yùn)算符優(yōu)先級與表達(dá)式示例詳解
這篇文章主要給大家介紹了關(guān)于JS運(yùn)算符優(yōu)先級與表達(dá)式的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
javascript實(shí)現(xiàn)簡約的頁面右下角點(diǎn)擊彈出窗口示例【測試可用】
這篇文章主要介紹了javascript實(shí)現(xiàn)的頁面右下角點(diǎn)擊彈出窗口功能,結(jié)合實(shí)例形式詳細(xì)分析了javascript頁面右下角點(diǎn)擊彈出窗口功能的相關(guān)步驟、原理與注意事項(xiàng),需要的朋友可以參考下2023-07-07

