JavaScript中的繼承方式

借用原型鏈

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'

上面的例子定義了兩個函數SuperTypeChildType,而它們的主要區別是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會獲得兩個屬性:namecolors,它們都是SuperType的實例屬性。只不過位於ChildType的原型中,當調用ChildType進行實例化的時候,又會調用一次SuperType函數,這一次又在新對象上創建了namecolors屬性。於是這兩個屬性就會屏蔽掉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原型上創建不必要的屬性。於此同時,原型鏈還能保持不變,可以正常使用instanceof和isPrototypeof()。普遍認爲寄生組合式繼承是最理想的繼承方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章