借用原型鏈
ECMAScript中描述了原型鏈的概念,並將原型鏈作爲實現繼承的主要方法。它的主要思想就是利用構造函數,原型和實例之間的關係。來實現一個引用類型繼承另一個引用類型的屬性和方法。我們知道,每個函數都有一個prototype
屬性,這個屬性指向該函數的原型對象。而當這個函數被當作構造函數進行實例化的時候,它的實例內部有一個[[Prototype]]
屬性,這個屬性指向這個構造函數的原型對象。如果將一個構造函數的原型等於另一個實例,那麼這個構造函數的原型就會包含一個指向另一個原型對象的指針。這種原型和實例之間的關係,正是我們借用原型鏈實現繼承的基本思想。
舉例說明:
function SuperType() {
this.superType = 'SuperType';
}
SuperType.prototype.getSuper = function() {
return this.superType;
}
function ChildType() {
this.childType = ';childType'
}
// 繼承了SuperType
ChildType.prototype = new SuperType();
let instance1 = new ChildType();
console.log(instance1.getSuper()); // 'SuperType'
上面的例子定義了兩個函數SuperType
和ChildType
,而它們的主要區別是ChildType
繼承了SuperType
。而這個繼承的實現是通過創建SuperType的實例,並將這個實例賦值給ChildType.prototype實現的。
通過原型鏈繼承的問題
原型鏈很強大,可以用它來實現繼承,但是它也偶一些問題,在通過原型實現繼承的時候,原型實際上會變成另一個構造函數的實例,那麼原先的實例屬性就會變成了原型的屬性,從而這個屬性變會稱爲共享的屬性,下面這個例子會很好的講述這個問題:
function SuperType() {
this.colors = ['red', 'green', 'blue']
}
function ChildType() {
}
ChildType.prototype = new SuperType();
let instance1 = new ChildType();
instance1.colors.push('yellow');
console.log(instance1.colors); // ["red", "green", "blue", "yellow"]
let instance2 = new ChildType();
console.log(instance2.colors); // ["red", "green", "blue", "yellow"]
在這個例子中SuperType
中定義了一個colors
屬性,SuperType
的每個實例都會包含這個colors屬性,當ChildType.prototype
成爲SuperType
的實例的時候,ChildType.prototype
中也包含這個colors
屬性,因此,當我們通過instance1.colors
進行操作的時候,引擎會順着原型鏈找到ChildType.prototype
中的colors
屬性,因此我們對colors
屬性進行的操作,也會影響到ChildType
的其他實例。因此實踐中很少會單獨使用原型鏈進行實現繼承。
借用構造函數
借用構造函數有時也被稱爲僞造對象或經典繼承,它的基本思想是在子類型構造函數中使用apply()
或call()
調用超類型(父類型)構造函數。舉例說明:
function SuperType() {
this.colors = ['red', 'green', 'blue']
}
function ChildType() {
SuperType.call(this);
}
let instance1 = new ChildType();
instance1.colors.push('yellow');
console.log(instance1.colors); // ["red", "green", "blue", "yellow"]
let instance2 = new ChildType();
console.log(instance2.colors); // ["red", "green", "blue"]
通過使用call()
方法,我們實際上是在新創建的ChildType
實例的環境下調用了SuperType
,這樣就會在新的ChildType
的實例上執行SuperType
中的初始化代碼,這樣每個實例都會有自己的colors
屬性副本。而藉助構造函數相比於借用原型鏈的一大優勢就是,子類型構造函數可以向超類型構造函數傳遞參數。看下面這個例子:
function SuperType(name) {
this.name = name;
}
function ChildType(name) {
SuperType.call(this, name);
}
let instance1 = new ChildType('Nick');
console.log(instance1.name); // 'Nick'
借用構造函數的問題
如果僅僅借用構造函數,那麼方法都只能在構造函數中進行定義,那麼函數的複用性將不復存在,而且對於子類型構造函數來講,定義在超類型原型上的方法對於子類型構造函數是不可見的。結果所有的類型都只能使用構造函數模式。因此借用構造函數的這個辦法也很少單獨使用。
組合繼承
組合繼承有時候也叫僞經典繼承,它是組合了原型鏈和構造函數兩種方式,從而發揮二者之長的一種繼承方式。基本思想是,利用原型鏈實現原型屬性和方法的繼承,從而通過構造函數來實現實例屬性的繼承(保證每個實例都有單獨的實例屬性,而不互相影響),舉例說明:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green', 'yellow'];
}
SuperType.prototype.getColors = function() {
return this.colors;
}
function ChildType(name) {
// 繼承屬性
SuperType.call(this, name);
}
// 繼承方法
ChildType.prototype = new SuperType();
let instance1 = new ChildType('Nick');
let instance2 = new ChildType('Cherry');
instance1.colors.push('black');
console.log(instance1.name); // 'Nick'
console.log(instance1.colors); // ' ["red", "green", "yellow", "black"]'
instance1.getColors(); // ' ["red", "green", "yellow", "black"]'
console.log(instance2.name); // 'Cherry'
console.log(instance2.colors); // ["red", "green", "yellow"]
組合繼承避免了單獨使用原型鏈和構造函數實現繼承時的缺點,時JavaScript中最常用的一種繼承方式。
原型式繼承
原型式繼承是2006年道格拉斯.克羅克福德提出的,他的基本思想是藉助原型基於已有的對象創建一個新對象,同時還不必因此創建自定義類型。他給出瞭如下的函數:
function object (o) {
function F() {};
F.prototype = o;
return new F();
}
let person = {
name: 'Nick',
friends: ['cherry', 'july'],
};
let anotherPerson = object(person);
anotherPerson.name = 'lily';
anotherPerson.friends.push('Tom');
let person2 = object(person);
person2.name = 'Jone';
person2.friends.push('Linda');
console.log(person.friends); // ["cherry", "july", "Tom", "Linda"]
原型式繼承要求你必須有一個對象作爲另一個對象的基礎,在這個例子中,我們把person對象作爲基礎,將person對象傳入到object()
函數中,這樣它就會返回另一個新的對象,這個新對象將person作爲原型,所以它的原型中就包含一個name屬性和一個friends屬性。這意味着,person.friends不僅是person所有,也被anotherPerson和person2所共享。
ES5通過新增的Object.create()
方法規範化了原型式繼承。這個方法接受兩個參數:一個用於作爲新對象原型的對象和(可選的)爲新對象定義額外屬性的對象。在傳入一個參數的情況下,Object.create()和上面的object()方法相同。
let person = {
name: 'Nick',
friends: ['cherry', 'july'],
};
let person1 = Object.create(person);
person1.name = 'Jhon';
person1.friends.push('cherry');
let person2 = Object.create(person);
person2.name = 'Lily';
person2.friends.push('Bob');
console.log(person.friends); // ["cherry", "july", "cherry", "Bob"]
Object.create()
方法的第二個參數和使用Object.defineProperties()
方法的第二個參數相同:每個屬性都是通過屬性描述符添加的。
let person = {
name: 'Nick',
friends: ['cherry', 'july'],
};
let anotherPerson = Object.create(person, {
name: {
value: 'Lily',
}
})
console.log(anotherPerson.name); // Lily
console.log(person.name); // Nick
在沒有必要興師動衆的創建構造函數,而只是想讓一個對象與另一個對象保持上面的這種關係,原型式繼承是完全勝任的。不過,包含引用類型的屬性始終是共享的,就像使用原型鏈繼承一樣。
寄生式繼承
寄生式繼承是與原型式繼承緊密相連的一種思想,它的基本思路是:創建一個僅用於封裝繼承過程的函數,在函數內部已某種方式來增強對象,最後在真的像它做了所有工作一樣返回對象。示例:
function object(o) {`
function F() {};
F.prototype = o;
return new F();
}
function createObject(original) {
let clone = object(original);
clone.sayHi = function() {
console.log('hi');
};
return clone;
}
let person = {
name: 'Nick',
friends: ['Tom', 'Jhon'],
}
let person1 = createObject(person);
person1.sayHi(); // 'hi'
在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種比較有用的模式。使用寄生式繼承來爲對象添加函數,會由於造成函數不能被複用而降低效率
寄生組合式繼承
前面說過,組合繼承是JavaScript中的經典繼承方式,但是它也有自己的不足。組合式繼承最大的問題就是無論什麼情況下都會調用兩次構造函數:一次是在創建子類型原型的時候,一次是在子類型構造函數的內部。看一下下面的這個示例:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'yellow', 'blue'];
}
SuperType.prototype.sayName = function() {
console.lofg(this.name)
}
function ChildType(name, age) {
SuperType.call(this, name); // 第二次調用SuperType()
this.age = age;
}
ChildType.prototype = new SuperType(); // 第一次調用SuperType()
ChildType.prototype.constructor = ChildType;
ChildType.prototype.sayAge = function() {
console.log(this.age);
}
在第一次調用SuperType
構造函數的時候,此時ChildType.prototype
會獲得兩個屬性:name和colors,它們都是SuperType的實例屬性。只不過位於ChildType的原型中,當調用ChildType
進行實例化的時候,又會調用一次SuperType函數,這一次又在新對象上創建了name
和colors
屬性。於是這兩個屬性就會屏蔽掉ChildType原型中的兩個同名屬性。因此就會造成有兩組name和colors屬性:一組在ChildType的原型中,一組在ChildType的實例中。這就是調用兩次構造函數的結果。而解決這個問題的方法就是——寄生式組合繼承。
寄生式組合繼承的基本模式如下:
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
function inheritPrototype(childType, superType) {
let prototype = object(superType.prototype); // 創建對象
prototype.constructor = childType; // 增強對象
childType.prototype = prototype; // 指定對象
}
實例中的inheritPrototype
完成了寄生組合式繼承最簡單的形式。它接受兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是創建超類型原型的一個副本,第二步是爲創建的副本添加constructor
屬性,彌補重寫原型後constructor屬性的丟失。最後一步將新創建的原型副本賦值給子類型的原型。這樣我們就可以調用inheritPrototype
來替換之前例子中爲子類型原型賦值的那一步,例如:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'yellow', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function ChildType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(ChildType, SuperType);
ChildType.prototype.sayAge = function() {
console.log(this.age);
}
let instance1 = new ChildType('cherry', 20);
instance1.sayName(); // 'cherry'
instance1.sayAge(); // 20
這個例子高效率的體現了它只調用了一次SuperType(
),並且因此避免了在ChildType原型上
創建不必要的屬性。於此同時,原型鏈還能保持不變,可以正常使用instanceo
f和isPrototypeof()
。普遍認爲寄生組合式繼承是最理想的繼承方式。