分享JavaScript?中的幾種繼承方式
前言:
說到JavaScript中的繼承,與之密切相關(guān)的就是原型鏈了,JavaScript中的繼承主要是通過原型鏈實(shí)現(xiàn)的。但是簡單的原型鏈繼承方式也存在一定的缺陷,在此借著《JavaScript高級程序設(shè)計(jì)(第四版)》一書,聊聊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(); // hi
5.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)文章
countUp.js實(shí)現(xiàn)數(shù)字滾動效果
這篇文章主要為大家詳細(xì)介紹了countUp.js實(shí)現(xiàn)數(shù)字滾動效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10JavaScript懶加載與預(yù)加載原理與實(shí)現(xiàn)詳解
這篇文章主要介紹了JavaScript懶加載與預(yù)加載,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09跟我學(xué)習(xí)javascript的undefined與null
跟我學(xué)習(xí)javascript的undefined與null,從定義上理解null和undefined,告訴大家提高undefined性能的方法,感興趣的小伙伴們可以參考一下2015-11-11JS判斷鍵盤是否按的回車鍵并觸發(fā)指定按鈕點(diǎn)擊操作的方法
下面小編就為大家?guī)硪黄狫S判斷鍵盤是否按的回車鍵并觸發(fā)指定按鈕點(diǎn)擊操作的方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02JavaScript中call、apply、bind實(shí)現(xiàn)原理詳解
其實(shí)在很多文章都會寫call,apply,bind,但個人覺著如果不弄懂原理,是很難理解透的,所以這篇文章主要介紹了JavaScript中call、apply、bind實(shí)現(xiàn)原理的相關(guān)資料,需要的朋友可以參考下2021-06-06