繼承 記錄一下 javascript 的各種繼承方式,個人用得比較多的還是原型鏈繼承和 ES6 的 extends。 原型鏈繼承 缺點: 在創建 Child 的實例時,無法向 Parents 傳參 父類裡面的引用類型被共用,個例修改導致所有實例都被修改 借用構造函數 為瞭解決上面的問題 ,經典繼承方 ...
繼承
記錄一下 javascript 的各種繼承方式,個人用得比較多的還是原型鏈繼承和 ES6 的 extends。
原型鏈繼承
// 原型模式
function Parents() {
this.name = 'Parents';
}
Parents.prototype.getName = function() {
return this.name;
}
// 原型鏈繼承
function Child() {}
Child.prototype = new Parents();
Child.prototype.constructor = Child;
let son = new Child();
son.getName(); // Parents;
缺點:
- 在創建 Child 的實例時,無法向 Parents 傳參
- 父類裡面的引用類型被共用,個例修改導致所有實例都被修改
function Parents() {
this.name = ['dad', 'mom'];
}
function Child() {}
Child.prototype = new Parents();
Child.prototype.constructor = Child;
// 無法向 Parents 傳參
let son = new Child();
let daughter = new Child();
// 父類裡面的引用類型被共用
son.name == daughter.name; // true
// 個例修改導致所有實例都被修改
son.name[0] = 'father';
daughter.name[0]; // "father"
借用構造函數
為瞭解決上面的問題 ,經典繼承方式被設計出來:
// 構造函數模式
function Parents(childName) {
this.childName = childName;
this.name = ['dad', 'mom'];
this.getName = function() {
return this.name;
}
}
// 借用構造函數繼承
function Child(name) {
Parents.call(this, name);
}
// 可以向 Parents 傳參
let son = new Child('son');
son.childName; // "son"
// 父類裡面的引用類型不會被共用
let daughter = new Child('daughter');
son.name[0] = 'father';
daughter.name[0]; // "dad"
缺點:方法都在構造函數里定義,每次創建 Child 實例,都要重新創建一次方法,函數無法得到復用。
組合繼承
為了彌補上面的缺點,於是乎組合繼承誕生了。
// 組合模式
function Parents(childName) {
this.childName = childName;
this.name = ['dad', 'mom'];
}
Parents.prototype.getName = function() {
return this.name;
}
// 組合繼承
function Child(name) {
Parents.call(this, name);
}
Child.prototype = new Parents();
Child.prototype.constructor = Child;
let son = new Child('son');
son.name[0] = 'father';
let daughter = new Child('daughter');
daufhter.name[0]; // "dad"
因為原型鏈能保持不變,所以instanceOf
和isProtypeOf()
也能識別基於組合繼承創建的對象。
原型式繼承
function obj(o) {
function F() {}
F.prototype = 0;
return new F();
}
這種方式就是在 obj 函數內部,創建一個臨時性的構造函數 F(),然後將傳入的對象作為這個構造函數的原型,最後返回這個臨時類的實例。
其實就是對傳入的對象進行一次淺複製,類似於 ES5 的 Object.create() 方法。
Object.create(proto, [propertiesObject]) 方法創建一個新對象,使用現有的對象來提供新創建的對象的
__proto__
。proto: 新創建對象的原型對象。
propertiesObject: 可選。如果沒有指定為 undefined,則是要添加到新創建對象的可枚舉屬性(即其自身定義的屬性,而不 是其原型鏈上的枚舉屬性)對象的屬性描述符以及相應的屬性名稱。這些屬性對應Object.defineProperties() 的第二個參數。
寄生式繼承
function createObj (o) {
var clone = Object.create(o);
clone.sayHi = function () {
console.log('hi');
}
return clone;
}
這種方式就是創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再返回增強過的對象。本質上還是對傳入的對象進行一次淺複製。
寄生組合式繼承
這種方式得先創建一個用於封裝繼承過程的函數。
function inheritPrototype(child, Parents) {
let prototype = Object.create(Parents.prototype);
prototype.constructor = child;
child.prototype = prototype
}
然後用上面的函數實現繼承:
function Parents(childName) {
this.childName = childName;
this.name = ['dad', 'mom'];
}
Parents.prototype.getName = function() {
return this.name;
}
function Child(name) {
Parents.call(this, name);
}
inheritPrototype(Child, Parents);
其實和組合繼承差不多,只是封裝了一個 inheritPrototype 函數使得在繼承的時候只在 Child 里調用一次 Parents 構造函數。
ES6 的 extends
ES6 引入了 class,可以通過extends
關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。但需要註意的是,ES6 的 class 只是語法糖,本質上還是 ES5 的通過修改原型鏈實現繼承的方法,具體可以去babel 試用來看看 babel 對於 class 的轉換。
class Parents {
// 構造函數
constructor(name) {
this.name = name;
}
// 靜態方法
static getName() {
return this.name;
}
// 方法
printName() {
console.log(this.name);
}
}
class Child extends Parents {
constructor(name, childName) {
// 調用父類的constructor(name)
super(name);
this.childName = childName;
}
printName() {
super.printName();
console.log(this.childName);
}
}
let c = new Child('mom', 'son');
c.printName();// mom son
ES5 的繼承,實質是先創造子類的實例對象
this
,然後再將父類的方法添加到this
上面(Parent.apply(this)
)。ES6 的繼承機制完全不同,實質是先將父類實例對象的屬性和方法,加到this
上面(所以必須先調用super
方法),然後再用子類的構造函數修改this
。
大多數瀏覽器的 ES5 實現之中,每一個對象都有
__proto__
屬性,指向對應的構造函數的prototype
屬性。Class 作為構造函數的語法糖,同時有prototype
屬性和__proto__
屬性,因此同時存在兩條繼承鏈。(1)子類的
__proto__
屬性,表示構造函數的繼承,總是指向父類。(2)子類
prototype
屬性的__proto__
屬性,表示方法的繼承,總是指向父類的prototype
屬性。
參考
《javascript高級程式設計》(三)