原文出自: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.stringify
和 JSON.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
在上面可以看出復制后的新對象 child
的 a
屬性和 b
的引用是獨立的拿穴,與 parent
的 a
和 b
毫無關(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ù)坤溃,并借用 call
或 apply
在內(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í)行了兩次换途,在使用 call
或 apply
繼承屬性時執(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ù) F
的 prototype
替換成了父類的原型现横,并創(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)方法直接會被子類繼承金赦,在子類的原型方法中使用父類的原型方法只需使用 this
或 supper
調(diào)用即可,此時 this
指向子類的實例对嚼,如果在子類的靜態(tài)方法中使用 this
或 supper
調(diào)用父類的靜態(tài)方法夹抗,此時 this
指向子類本身。