JavaScript 的繼承方式及優(yōu)缺點


原文出自:https://www.pandashen.com


前言

JavaScript 原本不是純粹的 “OOP” 語言蚁飒,因為在 ES5 規(guī)范中沒有類的概念,在 ES6 中才正式加入了 class 的編程方式咳榜,在 ES6 之前,也都是使用面向?qū)ο蟮木幊谭绞揭斎皇?JavaScript 獨有的面向?qū)ο缶幊袒哐埽疫@種編程方式是建立在 JavaScript 獨特的原型鏈的基礎(chǔ)之上的,我們本篇就將對原型鏈以及面向?qū)ο缶幊套畛S玫降睦^承進行刨析救恨。


繼承簡介

在 JavaScript 的中的面向?qū)ο缶幊堂潮玻^承是給構(gòu)造函數(shù)之間建立關(guān)系非常重要的方式,根據(jù) JavaScript 原型鏈的特點肠槽,其實繼承就是更改原本默認的原型鏈擎淤,形成新的原型鏈的過程。


復制的方式進行繼承

復制的方式進行繼承指定是對象與對象間的淺復制和深復制秸仙,這種方式到底算不算繼承的一種備受爭議嘴拢,我們也把它放在我們的內(nèi)容中,當作一個 “不正經(jīng)” 的繼承寂纪。

1席吴、淺復制

創(chuàng)建一個淺復制的函數(shù),第一個參數(shù)為復制的源對象捞蛋,第二個參數(shù)為目標對象孝冒。

// 淺復制方法
function extend(p, c = {}) {
    for (let k in p) {
        c[k] = p[k];
    }
    return c;
}

// 源對象
let parent = {
    a: 1,
    b: function() {
        console.log(1);
    }
};

// 目標對象
let child = {
    c: 2
};

// 執(zhí)行
extend(parent, child);
console.log(child); // { c: 2, a: 1, b: ? }

上面的 extend 方法在 ES6 標準中可以直接使用 Object.assign 方法所替代。

2拟杉、深復制

可以組合使用 JSON.stringifyJSON.parse 來實現(xiàn)庄涡,但是有局限性,不能處理函數(shù)和正則類型搬设,所以我們自己實現(xiàn)一個方法穴店,參數(shù)與淺復制相同。

// 深復制方法
function extendDeeply(p, c = {}) {
    for (let k in p) {
        if (typeof p[k] === "object" && typeof p[k] !== null) {
            c[k] = p[k] instanceof Array ? [] : {};
            extendDeeply(p[k], c[k]);
        } else {
            c[k] = p[k];
        }
    }
    return c;
}

// 源對象
let parent = {
    a: {
        b: 1
    },
    b: [1, 2, 3],
    c: 1,
    d: function() {
        console.log(1);
    }
};

// 執(zhí)行
let child = extendDeeply(parent);

console.log(child); // { a: {b: 1}, b: [1, 2, 3], c: 1, d: ? }
console.log(child.a === parent.a); // false
console.log(child.b === parent.b); // false
console.log(child.d === parent.d); // true

在上面可以看出復制后的新對象 childa 屬性和 b 的引用是獨立的拿穴,與 parentab 毫無關(guān)系泣洞,實現(xiàn)了深復制,但是 extendDeeply 函數(shù)并沒有對函數(shù)類型做處理默色,因為函數(shù)內(nèi)部執(zhí)行相同的邏輯指向不同引用是浪費內(nèi)存的斜棚。


原型替換

原型替換是繼承當中最簡單也是最直接的方式,即直接讓父類和子類共用同一個原型對象,一般有兩種實現(xiàn)方式弟蚀。

// 原型替換
// 父類
function Parent() {}

// 子類
function Child() {}

// 簡單粗暴的寫法
Child.prototype = Parent.prototype;

// 另一種種實現(xiàn)方式
Object.setPrototypeOf(Child.prototype, Parent.prototype);

上面這種方式 Child 的原型被替換掉蚤霞,Child 的實例可以直接調(diào)用 Parent 原型上的方法,實現(xiàn)了對父類原型方法的繼承义钉。

上面第二種方式使用了 Object.setPrototypeOf 方法昧绣,該方法是將傳入第一個參數(shù)對象的原型設(shè)置為第二個參數(shù)傳入的對象,所以我們第一個參數(shù)傳入的是 Child 的原型捶闸,將 Child 原型的原型設(shè)置成了 Parent 的原型夜畴,使父、子類原型鏈產(chǎn)生關(guān)聯(lián)删壮,Child 的實例繼承了 Parent 原型上的方法贪绘,在 NodeJS 中的內(nèi)置模塊 util 中用來實現(xiàn)繼承的方法 inherits,底層就是使用這種方式實現(xiàn)的央碟。

缺點:父類的實例也同樣可以調(diào)用子類的原型方法税灌,我們希望繼承是單向的,否則無法區(qū)分父亿虽、子類關(guān)系菱涤,這種方式一般是不可取的。


原型鏈繼承

原型鏈繼承的思路是子類的原型的原型是父類的原型洛勉,形成了一條原型鏈粘秆,建立子類與父類原型的關(guān)系。

// 原型鏈繼承
// 父類
function Parent(name) {
    this.name = name;
    this.hobby = ["basketball", "football"];
}

// 子類
function Child() {}

// 繼承
Child.prototype = new Parent();

上面用 Parent 的實例替換了 Child 自己的原型收毫,由于父類的實例原型直接指向 Parent.prototype攻走,所以也使父、子類原型鏈產(chǎn)生關(guān)聯(lián)此再,子類實例繼承了父類原型的方法昔搂。

缺點 1:只能繼承父類原型上的方法,卻無法繼承父類上的屬性引润。
缺點 2:由于原型對象被替換,原本原型的 constructor 屬性丟失痒玩。
缺點 3:如果父類的構(gòu)造函數(shù)中有屬性淳附,則創(chuàng)建的父類的實例也會有這個屬性,用這個實例的作為子類的原型蠢古,這個屬性就變成了所有子類實例所共有的奴曙,這個屬性可能是多余的,并不是我們想要的草讶,也可能我們希望它不是共有的洽糟,而是每個實例自己的。


構(gòu)造函數(shù)繼承

構(gòu)造函數(shù)繼承又被國內(nèi)的開發(fā)者叫做 “經(jīng)典繼承”。

// 構(gòu)造函數(shù)繼承
// 父類
function Parent(name) {
    this.name = name;
}

// 子類
function Child() {
    Parent.apply(this, arguments);
}

let c = new Child("Panda");
console.log(c); // { name: 'Panda' }

構(gòu)造函數(shù)繼承的原理就是在創(chuàng)建 Child 實例的時候執(zhí)行了 Child 構(gòu)造函數(shù)坤溃,并借用 callapply 在內(nèi)部執(zhí)行了父類 Parent拍霜,并把父類的屬性創(chuàng)建給了 this,即子類的實例薪介,解決了原型鏈繼承不能繼承父類屬性的缺點祠饺。

缺點:子類的實例只能繼承父類的屬性,卻不能繼承父類的原型的方法汁政。


構(gòu)造函數(shù)原型鏈組合繼承

為了使子類既能繼承父類原型的方法道偷,又能繼承父類的屬性到自己的實例上,就有了這種組合使用的方式记劈。

// 構(gòu)造函數(shù)原型鏈組合繼承
// 父類
function Parent(name) {
    this.name = name;
}

Parent.prototype.sayName = function() {
    console.log(this.name);
};

// 子類
function Child() {
    Parent.apply(this, arguments);
}

// 繼承
Child.prototype = new Parent();

let c = new Child("Panda");
console.log(c); // { name: 'Panda' }
c.sayName(); // Panda

這種繼承看似完美勺鸦,但是之前 constructor 丟失和子類原型上多余共有屬性的問題還是沒有解決,在這基礎(chǔ)上又產(chǎn)生了新的問題目木。

缺點:父類被執(zhí)行了兩次换途,在使用 callapply 繼承屬性時執(zhí)行一次,在創(chuàng)建實例替換子類原型時又被執(zhí)行了一次嘶窄。


原型式繼承

原型式繼承主要用來解決用父類的實例替換子類的原型時共有屬性的問題怀跛,以及父類構(gòu)造函數(shù)執(zhí)行兩次的問題,也就是說通過原型式繼承能保證子類的原型是 “干凈的”柄冲,而保證只在繼承父類的屬性時執(zhí)行一次父類吻谋。

// 原型式繼承
// 父類
function Parent(name) {
    this.name = name;
}

// 子類
function Child() {
    Parent.apply(this, arguments);
}

// 繼承函數(shù)
function create(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}

// 繼承
Child.prototype = create(Parent.prototype);

let c = new Child("Panda");
console.log(c); // { name: 'Panda' }

原型式繼承其實是借助了一個中間的構(gòu)造函數(shù),將中間構(gòu)造函數(shù) Fprototype 替換成了父類的原型现横,并創(chuàng)建了一個 F 的實例返回漓拾,這個實例是不具備任何屬性的(干凈的),用這個實例替換子類的原型戒祠,因為這個實例的原型指向 F 的原型骇两,F 的原型同時又是父類的原型對象,所以子類實例繼承了父類原型的方法姜盈,父類只在創(chuàng)建子類實例的時候執(zhí)行了一次低千,省去了創(chuàng)建父類實例的過程。

原型式繼承在 ES5 標準中被封裝成了一個專門的方法 Object.create馏颂,該方法的第一個參數(shù)與上面 create 函數(shù)的參數(shù)相同示血,即要作為原型的對象,第二個參數(shù)則可以傳遞一個對象救拉,會把對象上的屬性添加到這個原型上难审,一般第二個參數(shù)用來彌補 constructor 的丟失問題,這個方法不兼容 IE 低版本瀏覽器亿絮。


寄生式繼承

寄生式繼承就是用來解決子統(tǒng)一為原型式繼承中返回的對象統(tǒng)一添加方法的問題告喊,只是在原型式繼承的基礎(chǔ)上做了小小的修改麸拄。

// 寄生式繼承
// 父類
function Parent(name) {
    this.name = name;
}

// 子類
function Child() {
    Parent.apply(this, arguments);
}

// 繼承函數(shù)
function create(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}

// 將子類方法私有化函數(shù)
function creatFunction(obj) {
    // 調(diào)用繼承函數(shù)
    let clone = create(obj);
    // 子類原型方法(多個)
    clone.sayName = function() {};
    clone.sayHello = function() {};

    return clone;
}

// 繼承
Child.prototype = creatFunction(Parent.prototype);

缺點:因為寄生式繼承最后返回的是一個對象,如果用一個變量直接來接收它黔姜,那相當于添加的所有方法都變成這個對象自身的了拢切,如果創(chuàng)建了多個這樣的對象,無法實現(xiàn)相同方法的復用地淀。


寄生組合式繼承

// 寄生組合式繼承
// 父類
function P(name, age) {
    this.name = name;
    this.age = age;
}

P.prototype.headCount = 1;
P.prototype.eat = function() {
    console.log("eating...");
};

// 子類
function C(name, age) {
    P.apply(this, arguments);
}

// 寄生組合式繼承方法
function myCreate(Child, Parent) {
    function F() {}
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    // 讓 Child 子類的靜態(tài)屬性 super 和 base 指向父類的原型
    Child.super = Child.base = Parent.prototype;
}

// 調(diào)用方法實現(xiàn)繼承
myCreate(C, P);

// 向子類原型添加屬性方法失球,因為子類構(gòu)造函數(shù)的原型被替換,所以屬性方法仍然在替換之后
C.prototype.language = "javascript";
C.prototype.work = function() {
    console.log("writing code use " + this.language);
};
C.work = function() {
    this.super.eat();
};

// 驗證繼承是否成功
let f = new C("nihao", 16);
f.work();
C.work();

// writing code use javascript
// eating...

寄生組合式繼承基本規(guī)避了其他繼承的大部分缺點帮毁,應(yīng)該比較強大了实苞,也是平時使用最多的一種繼承,其中 Child.super 方法的作用是為了在調(diào)用子類靜態(tài)屬性的時候可以調(diào)用父類的原型方法烈疚。

缺點:子類沒有繼承父類的靜態(tài)方法黔牵。


class...extends... 繼承

在 ES6 規(guī)范中有了類的概念,使繼承變得容易爷肝,在規(guī)避上面缺點的完成繼承的同時猾浦,又在繼承時繼承了父類的靜態(tài)屬性。

// class...extends... 繼承
// 父類
class P {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sayName() {
        console.log(this.name);
    }
    static sayHi() {
        console.log("Hello");
    }
}

// 子類繼承父類
class C extends P {
    constructor(name, age) {
        supper(name, age); // 繼承父類的屬性
    }
    sayHello() {
        P.sayHi();
    }
    static sayHello() {
        super.sayHi();
    }
}

let c = new C("jack", 18);

c.sayName(); // jack
c.sayHello(); // Hello
C.sayHi(); // Hello
C.sayHello(); // Hello

在子類的 constructor 中調(diào)用 supper 可以實現(xiàn)對父類屬性的繼承灯抛,父類的原型方法和靜態(tài)方法直接會被子類繼承金赦,在子類的原型方法中使用父類的原型方法只需使用 thissupper 調(diào)用即可,此時 this 指向子類的實例对嚼,如果在子類的靜態(tài)方法中使用 thissupper 調(diào)用父類的靜態(tài)方法夹抗,此時 this 指向子類本身。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纵竖,一起剝皮案震驚了整個濱河市漠烧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌靡砌,老刑警劉巖已脓,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異通殃,居然都是意外死亡度液,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門画舌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來堕担,“玉大人,你說我怎么就攤上這事骗炉≌毡Γ” “怎么了蛇受?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵句葵,是天一觀的道長。 經(jīng)常有香客問我,道長乍丈,這世上最難降的妖魔是什么剂碴? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮轻专,結(jié)果婚禮上忆矛,老公的妹妹穿的比我還像新娘。我一直安慰自己请垛,他們只是感情好催训,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宗收,像睡著了一般漫拭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上混稽,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天采驻,我揣著相機與錄音,去河邊找鬼匈勋。 笑死礼旅,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的洽洁。 我是一名探鬼主播痘系,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼诡挂!你這毒婦竟也來了碎浇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤璃俗,失蹤者是張志新(化名)和其女友劉穎奴璃,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體城豁,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡苟穆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了唱星。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雳旅。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖间聊,靈堂內(nèi)的尸體忽然破棺而出攒盈,到底是詐尸還是另有隱情,我是刑警寧澤哎榴,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布型豁,位于F島的核電站僵蛛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏迎变。R本人自食惡果不足惜充尉,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衣形。 院中可真熱鬧驼侠,春花似錦、人聲如沸谆吴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽句狼。三九已至相速,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鲜锚,已是汗流浹背突诬。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芜繁,地道東北人旺隙。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像骏令,于是被迫代替她去往敵國和親蔬捷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容