分享JavaScript?中的幾種繼承方式
前言:
說(shuō)到JavaScript中的繼承,與之密切相關(guān)的就是原型鏈了,JavaScript中的繼承主要是通過(guò)原型鏈實(shí)現(xiàn)的。但是簡(jiǎn)單的原型鏈繼承方式也存在一定的缺陷,在此借著《JavaScript高級(jí)程序設(shè)計(jì)(第四版)》一書(shū),聊聊JavaScript中的幾種繼承方式
一、原型鏈
ECMA-262 把原型鏈定義為ECMAScript的主要繼承方式,其基本思想就是通過(guò)原型繼承多個(gè)引用類型的屬性和方法。
在此回顧一下原型、構(gòu)造函數(shù)、實(shí)例之間的關(guān)系:
每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型有一個(gè)屬性指回構(gòu)造函數(shù),實(shí)例有一個(gè)內(nèi)部指針指向原型。
有關(guān)原型和原型鏈的知識(shí)這里先不多說(shuō)了,這里來(lái)談?wù)勗玩湹囊恍﹩?wèn)題。
1.1 原型鏈的問(wèn)題
- 原型鏈主要問(wèn)題出現(xiàn)在原型中包含引用值的時(shí)候。因?yàn)樵蜕系膶傩詴?huì)在所有屬性之間共享,對(duì)于原型上的引用值,實(shí)例繼承的是指向該對(duì)象的引用,所以在實(shí)例中修改該屬性時(shí),會(huì)影響原型上的屬性。
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
對(duì)象實(shí)例自身并沒(méi)有colors
屬性,而是繼承自原型,所以向colors
中添加"green"影響到的原型上的colors
。這就導(dǎo)致son2
訪問(wèn)colors
屬性時(shí)值為['red', 'green']
。
所以,若原型上屬性為引用值時(shí),在實(shí)例中對(duì)該屬性修改時(shí)會(huì)影響原型屬性。
但是需要注意下面這種情況:
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
。
- 原型鏈的另一個(gè)問(wèn)題是,子類型在實(shí)例化時(shí)不能給父類型的構(gòu)造函數(shù)傳參。即不能在不影響其他對(duì)象實(shí)例的情況下傳遞參數(shù)給父類的構(gòu)造函數(shù)。
那上面的代碼來(lái)說(shuō)就是,在創(chuàng)建Son
對(duì)象實(shí)例的時(shí)候,不能指定colors
的值。
綜上所述:由于引用值和傳參問(wèn)題,原型鏈一般不會(huì)被單獨(dú)使用。
二、盜用構(gòu)造函數(shù)
為了解決原型包含引用值所導(dǎo)致的問(wèn)題,出現(xiàn)了一種叫作"盜用構(gòu)造函數(shù)"(constructor stealing)的技術(shù)。
2.1 基本思想
在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù)。主要是通過(guò)
call
和apply
來(lái)實(shí)現(xiàn)。
function Father() { this.colors = ['red']; } function Son() { // 在此通過(guò)call()調(diào)用父類構(gòu)造函數(shù) Father.call(this); } let son1 = new Son(); console.log(son1.colors); // ['red'] // 說(shuō)明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ù)的過(guò)程可知,會(huì)將函數(shù)中的this
指向新創(chuàng)建的實(shí)例。所以Father.call(this);
相當(dāng)于實(shí)例調(diào)用了Father
方法,然后添加了自身屬性colors
。所以后續(xù)son1.colors.push('green');
并不會(huì)影響到其他實(shí)例。
2.2 可向父類構(gòu)造函數(shù)傳參
盜用構(gòu)造函數(shù)的另外一個(gè)優(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ù)的問(wèn)題
盜用構(gòu)造函數(shù)的主要問(wèn)題如下:
- 所有方法必須在構(gòu)造函數(shù)中定義,所以方法不能重用。(即:即使功能相同的方法,每個(gè)實(shí)例上對(duì)應(yīng)的該方法不是同一個(gè)函數(shù)對(duì)象)
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
- 子類不能訪問(wèn)到父類原型上的方法。因?yàn)樽宇悆H僅只是調(diào)用父類構(gòu)造函數(shù),并沒(méi)有設(shè)置原型指向父類實(shí)例。子類和父類之間并沒(méi)有建立原型關(guān)系。
let son = new Son(); console.log(son instanceof Father) // false
綜上所述:單獨(dú)使用盜用構(gòu)造函數(shù)也是不可行的。
三、組合繼承(偽經(jīng)典繼承)
3.1 基本思想
組合繼承綜合了原型鏈和盜用構(gòu)造函數(shù),使用原型鏈繼承原型上的屬性和方法,通過(guò)盜用構(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']} // 每個(gè)實(shí)例都有自身的 colors 屬性 console.log(son1.colors === son2.colors) // false // 實(shí)例間共享sayHello方法 console.log(son1.sayHello === son2.sayHello) // true
通過(guò)調(diào)用父類構(gòu)造函數(shù),每個(gè)實(shí)例都有“自身”的原型屬性(例如:colors),所以通過(guò)引用修改對(duì)應(yīng)的對(duì)象時(shí),不會(huì)影響其他實(shí)例,因?yàn)槊總€(gè)實(shí)例的引用值屬性指向的對(duì)象不同。此外,通過(guò)原型鏈也實(shí)現(xiàn)了所以實(shí)例之間共享同一方法。
3.2 組合繼承的問(wèn)題
雖然組合繼承彌補(bǔ)了原型鏈和盜用構(gòu)造函數(shù)的不足,但是組合繼承也存在效率問(wèn)題:
- 父類的構(gòu)造函數(shù)會(huì)被調(diào)用兩次
- 一次時(shí)在創(chuàng)建子類原型的時(shí)候被調(diào)用
- 另一次是實(shí)例化子類對(duì)象時(shí)在子類構(gòu)造函數(shù)中被調(diào)用
- 子類原型上存在不必要的屬性
console.log(Son.prototype); // Father?{name: undefined, colors: Array(1)}
緊接著上述代碼,我們可以看到子類構(gòu)造函數(shù)的原型對(duì)象上有name
和colors
屬性,但是每個(gè)Son
對(duì)象實(shí)例上都有自身的name
和colors
屬性,并不是繼承自原型。所以,子類構(gòu)造函數(shù)的原型對(duì)象上有name
和colors
屬性是多余的。
- 子類構(gòu)造函數(shù)原型(prototype)上的
constructor
屬性丟失
console.log(Son.prototype.constructor === Son) // false
修改構(gòu)造函數(shù)的原型都會(huì)出現(xiàn)這種問(wèn)題。
四、原型式繼承
4.1 基本思想
function object(o) { function F(); F.prototype = o; return new F(); }
其實(shí)就是在創(chuàng)建一個(gè)對(duì)象時(shí),指定該對(duì)象的原型。
4.2 Object.create()
在ECMAScript 5 中增加了Object.create()
方法,對(duì)原型式繼承進(jìn)行了規(guī)范化
Object.create()
方法創(chuàng)建一個(gè)新對(duì)象,使用現(xiàn)有的對(duì)象來(lái)提供新創(chuàng)建的對(duì)象的__proto__。
(1)語(yǔ)法
Object.create(proto,[propertiesObject])
proto
: 新創(chuàng)建對(duì)象的原型對(duì)象。propertiesObject
: 可選。需要傳入一個(gè)對(duì)象,該對(duì)象的屬性類型參照Object.defineProperties()
的第二個(gè)參數(shù)。如果該參數(shù)被指定且不為undefined
,該傳入對(duì)象的自有可枚舉屬性(即其自身定義的屬性,而不是其原型鏈上的枚舉屬性)將為新創(chuàng)建的對(duì)象添加指定的屬性值和對(duì)應(yīng)的屬性描述符。- 返回值:一個(gè)新對(duì)象,帶著指定的原型對(duì)象和屬性。
(2)示例
o = Object.create(Object.prototype, { // foo會(huì)成為所創(chuàng)建對(duì)象的數(shù)據(jù)屬性 foo: { writable:true, configurable:true, value: "hello" }, // bar會(huì)成為所創(chuàng)建對(duì)象的訪問(wèn)器屬性 bar: { configurable: false, get: function() { return 10 }, set: function(value) { console.log("Setting `o.bar` to", value); } } });
(3)手動(dòng)實(shí)現(xiàn)
function objectCreate(proto, propertiesObject=undefined){ // 構(gòu)造函數(shù) function F() { } // 構(gòu)造函數(shù)原型 prototype 鏈接到proto對(duì)象 F.prototype = proto; // 創(chuàng)建對(duì)象 const obj = new F(); // 若參數(shù) propertiesObject 被指定且不為 undefined if (propertiesObject !== undefined) { // 新創(chuàng)建的對(duì)象添加指定的屬性值和對(duì)應(yīng)的屬性描述符。 Object.defineProperties(obj, propertiesObject); } return obj; }
五、寄生式繼承
5.1 基本思想
創(chuàng)建一個(gè)實(shí)現(xiàn)繼承的函數(shù),以某種方式增強(qiáng)對(duì)象,然后返回這個(gè)對(duì)象。
function createAnother(original) { // 通過(guò)調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象 let clone = Object(original); // 以某種方式增強(qiáng)這個(gè)對(duì)象 clone.sayHi = function() { console.log('hi'); }; // 返回增強(qiáng)的對(duì)象 return clone; }
個(gè)人理解:寄生式繼承就是通過(guò)一個(gè)函數(shù),以當(dāng)前對(duì)象為基礎(chǔ),創(chuàng)建一個(gè)新的對(duì)象,并為新的對(duì)象添加新的方法。
let obj = {}; let anotherObj = createAnother(obj); anotherObj.sayHi(); // hi
5.2 寄生式繼承
與盜用構(gòu)造函數(shù)類似,寄生式繼承中給對(duì)象新增的函數(shù)不能被重用。
六、寄生式組合繼承
針對(duì)第三節(jié)中組合繼承存在的問(wèn)題,可以通過(guò)寄生式組合繼承來(lái)解決。
6.1 基本思想
不通過(guò)調(diào)用父類構(gòu)造函數(shù)給子類原型賦值,而是得到父類原型的一個(gè)副本。即使用寄生式繼承來(lái)繼承父類原型,然后將返回的新對(duì)象賦值給子類原型。
function inheritPrototype(subType, SuperType) { // 創(chuàng)建對(duì)象 let prototype = Object(SuperType.prototype); // 增強(qiáng)對(duì)象(防止修改原型導(dǎo)致constructor丟失) prototype.constructor = subType; // 賦值對(duì)象 subType.prototype = prototype }
subType
:子類構(gòu)造函數(shù)SuperType
:父類構(gòu)造函數(shù)
如上代碼:
- 首先創(chuàng)建一個(gè)父類原型的副本
- 在副本上添加
constructor
屬性,防止在修改原型時(shí)丟失了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í)例化時(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
如上代碼,寄生式組合繼承解決了組合繼承存在的一些問(wèn)題。綜上,寄生式組合繼承可以算是引用類型繼承的最佳模式。
但是,關(guān)于寄生式組合需要注意的一點(diǎn)是:寄生式繼承函數(shù)在創(chuàng)建對(duì)象副本時(shí),如果使用的是Object()
函數(shù),對(duì)于Object()
函數(shù)如果給定值是一個(gè)已經(jīng)存在的對(duì)象,則會(huì)返回這個(gè)已經(jīng)存在的值(相同地址)。所以函數(shù)中prototype.constructor = subType;
會(huì)修改父類原型上的constructor
屬性。
console.log(Father.prototype.constructor) // ? Son(name) {// 繼承屬性 Father.call(this, name);} console.log(Father.prototype.constructor === Father) // false
但是,這并不會(huì)影響父類對(duì)象實(shí)例的創(chuàng)建
console.log(new Father('haha')) // Father?{name: 'haha', colors: Array(1)}
到此這篇關(guān)于分享JavaScript 中的幾種繼承方式的文章就介紹到這了,更多相關(guān)JS繼承方式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
countUp.js實(shí)現(xiàn)數(shù)字滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了countUp.js實(shí)現(xiàn)數(shù)字滾動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10前端算法題解leetcode114二叉樹(shù)展開(kāi)為鏈表
這篇文章主要為大家介紹了前端算法題解leetcode114二叉樹(shù)展開(kāi)為鏈表,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09JavaScript懶加載與預(yù)加載原理與實(shí)現(xiàn)詳解
這篇文章主要介紹了JavaScript懶加載與預(yù)加載,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09ECharts柱狀圖過(guò)多添加滾動(dòng)條的步驟(親測(cè)可用)
這篇文章主要介紹了ECharts柱狀圖過(guò)多添加滾動(dòng)條的步驟(親測(cè)可用),添加echarts柱狀圖滾動(dòng)條,首先添加js用來(lái)判斷當(dāng)前視圖要顯示幾個(gè)及是否顯示滾動(dòng)條,本文結(jié)合實(shí)例代碼介紹的非常詳細(xì),需要的朋友參考下吧2024-01-01跟我學(xué)習(xí)javascript的undefined與null
跟我學(xué)習(xí)javascript的undefined與null,從定義上理解null和undefined,告訴大家提高undefined性能的方法,感興趣的小伙伴們可以參考一下2015-11-11JS判斷鍵盤是否按的回車鍵并觸發(fā)指定按鈕點(diǎn)擊操作的方法
下面小編就為大家?guī)?lái)一篇JS判斷鍵盤是否按的回車鍵并觸發(fā)指定按鈕點(diǎn)擊操作的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02JavaScript中call、apply、bind實(shí)現(xiàn)原理詳解
其實(shí)在很多文章都會(huì)寫(xiě)call,apply,bind,但個(gè)人覺(jué)著如果不弄懂原理,是很難理解透的,所以這篇文章主要介紹了JavaScript中call、apply、bind實(shí)現(xiàn)原理的相關(guān)資料,需要的朋友可以參考下2021-06-06