JS中的六種繼承方式以及優(yōu)缺點總結(jié)
前言
繼承是JS世界中必不可少的一個環(huán)節(jié),號稱JS的三座大山之一,使用這種方式我們可以更好地復(fù)用以前的開發(fā)代碼,縮短開發(fā)的周期、提升開發(fā)效率
在ES6之前,JS中的類都是通過構(gòu)造函數(shù)模擬的,并不存在真正意義上的類,雖然ES6的類知識一個語法糖😂,這個時期的類是可以當(dāng)作函數(shù)直接使用的,到了ES6之后,類是不可以再當(dāng)作函數(shù)使用了
在開始聊繼承之前,首先需要明確的是類中存在兩種屬性:實例上的屬性和公共屬性,接下來談到的所有繼承方式都是圍繞這兩點來展開
function Animal(name) { // 實例上的屬性 this.name = name; } // 公共屬性 Animal.prototype.eat = function() { // todo ... }
如何避免將ES6之前的構(gòu)造函數(shù)直接當(dāng)作函數(shù)調(diào)用?
ES5時期解決方案:
function Animal() { // 若是直接調(diào)用, 不是使用 new調(diào)用, 拋出異常 if (!(this instanceof Animal)) { // new的原理, 使用new的時候, this是Animal的實例, 則 this instanceof Animal 為true throw new Error("請勿直接調(diào)用構(gòu)造函數(shù)"); } }
ES6之后解決方案:
function Animal() { // 若是使用new, 則new.target指向自身, 否則為undefined, 但是在繼承的時候不能使用,因為繼承實例上屬性的時候, 原來的es5是使用 Animal.call(this)的方式 if (!new.target) { throw new Error("請勿直接調(diào)用構(gòu)造函數(shù)"); } }
上述兩種方案都可以解決直接當(dāng)作函數(shù)調(diào)用,若是直接調(diào)用控制臺會報錯:Uncaught Error: 請勿直接調(diào)用構(gòu)造函數(shù)
接下來便一起看看JS中有哪些繼承方式
原型鏈繼承
原型鏈繼承是比較常見的繼承方式之一,其中涉及的構(gòu)造函數(shù)、原型和實例,三者之間存在著一定的關(guān)系,即每一個構(gòu)造函數(shù)都有一個原型對象,原型對象又包含一個指向構(gòu)造函數(shù)的指針,而實例則包含一個原型對象的指針。
function Person(name) { this.name = name; this.permission = ["user", "salary", "vacation"]; } Person.prototype.say = function () { console.log(`${this.name} 說話了`); }; function Staff(age) { this.age = age; } Staff.prototype = new Person("張三"); const zs = new Staff(12); console.log(zs.name); // 張三 zs.say(); // 張三 說話了
此時代碼是符合期望,接下來再創(chuàng)建一個實例并修改name和permission
const zs = new Staff(12); const zs2 = new Staff(18); zs.permission.pop() zs.name = '李四'; console.log(zs.name); console.log(zs2.name); console.log(zs.permission); console.log(zs2.permission);
前兩個分別輸出是:李四、張三,后面兩個輸出結(jié)果一致,都為["user", "salary"],為什么會出現(xiàn)這種情況呢?
當(dāng)執(zhí)行zs.name = '李四';時,其實這個時候是賦值操作,賦值之后zs變?yōu)?/p>
而zs2.name是通過原型鏈繼續(xù)查找,因此前面的兩個輸出是李四、張三
通過console.log(zs.__proto__ === zs2.__proto__);輸出為true,可以得知兩個實例使用的是同一個原型對象Person,他們的內(nèi)存空間是共享的,當(dāng)一個發(fā)生變化時,另外一個也隨之進行了變化
通過上述發(fā)現(xiàn)原型鏈繼承存在一些缺點
構(gòu)造函數(shù)繼承
構(gòu)造函數(shù)通常時借助call、apply來完成繼承
function Person(name) { this.name = name; this.permission = ["user", "salary", "vacation"]; } Person.prototype.say = function () { console.log(`${this.name} 說話了`); }; function Staff(name, age) { Person.call(this, name); this.age = age; } Staff.prototype.eat = function () { console.log('吃東西啦~~~'); } const zs = new Staff("張三", 12); console.log(zs);
上述代碼控制臺輸出:
可以看到不僅擁有Staff的屬性和方法,同時也繼承了Person的屬性,因為每次實例化的時候都會調(diào)用Person.call(this, name);,可以解決原型鏈繼承的問題
此時調(diào)用Person原型上的方法
zs.say()
這個時候控制臺會報錯:Uncaught TypeError: zs.say is not a function
組合繼承(原型鏈繼承和構(gòu)造函數(shù)繼承組合)
原型鏈繼承和構(gòu)造函數(shù)繼承都存在各自的問題和優(yōu)勢,結(jié)合兩種繼承方式便生成了組合繼承
function Person(name) { this.name = name; this.permission = ["user", "salary", "vacation"]; } Person.prototype.say = function () { console.log(`${this.name} 說話了`); }; function Staff(name, age) { // 第二次執(zhí)行 Person Person.call(this, name); this.age = age; } Staff.prototype.eat = function () { console.log("吃東西啦~~~"); }; // 第一次執(zhí)行 Person Staff.prototype = new Person(); // 若是不將Staff constructor指回到Staff, 此時的Staff實例zs.constructor則指向Person Staff.prototype.constructor = Staff; const zs = new Staff("張三", 12); const ls = new Staff("李四", 12); zs.permission.pop(); console.log(zs.permission); console.log(ls.permission); zs.say(); ls.say();
暫時控制臺的輸出都是正常的,也將上述兩種繼承的缺點解決了,但是此時又新增了兩個個問題:
- Person被執(zhí)行了兩次,分別為:Person.call(this, name)和new Person(),期望執(zhí)行一次,多執(zhí)行的一次便會造成一次性能開銷
- 在之前Staff.prototype = new Person()定義一些公共屬性和方法時會被覆蓋掉,例如不能實例調(diào)用zs.eat(),控制臺會報錯Uncaught TypeError: zs.eat is not a function,若是在其之后定義則會污染Person
寄生式繼承
通過利用Object.create獲得一份目標(biāo)對象的淺拷貝,然后添加一些方法避免污染基類,主要是解決組合繼承的第二個問題
主要將如下兩行代碼進行替換
Staff.prototype = new Person(); Staff.prototype.constructor = Staff;
替換為:
Staff.prototype = Object.create(Person.prototype, { constructor: { // 若是不將Staff constructor指回到Staff, 此時的Staff實例zs.constructor則指向Person value: Staff, }, });
組合寄生式繼承
到目前為止,還有一個兩次實例化Person的問題沒有解決,接下來的組合寄生式繼承可完美解決上述問題,這也是ES6之前所有繼承方式中最優(yōu)的繼承方式
完整代碼如下:
function Person(name) { this.name = name; this.permission = ["user", "salary", "vacation"]; } Person.prototype.say = function () { console.log(`${this.name} 說話了`); }; function Staff(name, age) { Person.call(this, name); this.age = age; } Staff.prototype = Object.create(Person.prototype, { constructor: { // 若是不將Staff constructor指回到Staff, 此時的Staff實例zs.constructor則指向Person value: Staff, }, }); Staff.prototype.eat = function () { console.log("吃東西啦~~~"); };
其實繼承時,改變Staff.prototype指向并不止上述這些方式,還有一些其他方法
- prototype.__proto__方式
Staff.prototype.__proto__ = Person.prototype
prototype.__proto__存在兼容性問題,自己找不到,通過原型鏈繼續(xù)向上查找,此時Animal和Tiger不會再共享同一地址,不會相互影響
- Object.setPrototypeOf方式
Object.setPrototypeOf(Staff.prototype, Person.prototype)
es6語法, 存在兼容性,其原理就是原理就是 prototype.__proto__方式
extends繼承
在ES6之后,可以使用extends進行繼承,這也是目前開發(fā)中最常使用的方式,雖然目前瀏覽器支持度并不理想,但是在工程化如此完善的今天,這些都已經(jīng)不是制約使用其的理由
class Person { constructor(name) { this.name = name; this.permission = ["user", "salary", "vacation"]; } say() { console.log(`${this.name} 說話了`); } } class Staff extends Person { constructor(name, age) { super(name); this.age = age; } eat() { console.log("吃東西啦~~~"); } }
其實ES6的繼承通過babel編譯之后,采用也是組合寄生式繼承,因此我們需要重點掌握其繼承原理。
總結(jié)
到此這篇關(guān)于JS中六種繼承方式以及優(yōu)缺點的文章就介紹到這了,更多相關(guān)JS繼承方式及優(yōu)缺點內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript實現(xiàn)焦點滾動圖效果 具體方法
以下JS代碼實現(xiàn)了焦點滾動圖的效果方法,有需要的朋友可以參考一下2013-06-06JS Array創(chuàng)建及concat()split()slice()的使用方法
下面小編就為大家?guī)硪黄狫S Array創(chuàng)建及concat()split()slice()的使用方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-06-06JS使用單鏈表統(tǒng)計英語單詞出現(xiàn)次數(shù)
這篇文章主要為大家詳細介紹了JS使用單鏈表統(tǒng)計英語單詞出現(xiàn)次數(shù)的相關(guān)資料,列出所有單詞及其出現(xiàn)次數(shù),感興趣的小伙伴們可以參考一下2016-06-06Js中parentNode,parentElement,childNodes,children之間的區(qū)別
這篇文章主要是對Js中parentNode,parentElement,childNodes,children的區(qū)別進行了詳細的分析介紹,需要的朋友可以過來參考下,希望對大家有所幫助2013-11-11