js - 經常使用的繼承

零、序言

  參考資料:JavaScript經常使用八種繼承方案javascript

  注:1.此篇筆記是站在上述資料的肩膀上的一篇小結;html

    2.閱讀以前建議溫習一下 js 中的 prototype 和 constructor;(js - __proto__ 、 prototype和constructor)
java

 

1、原型鏈上的繼承(new)

function Father() {
  this.fatherValue = true;
  this.colors = ['red', 'yellow', 'green'];
}

Father.prototype.getFatherValue = function() {
  return this.fatherValue;
}

function Son() {
  this.sonValue = false;
}

// 如下兩句須要按照順序寫,避免意外的覆蓋
Son.prototype = new Father(); // 核心,這裏創建繼承關係

Son.prototype.getSonValue = function() { // 定義須要的屬性/方法
  return this.sonValue;
}

var instance1 = new Son();
console.log(instance1)

 

  

  這個方法的原理是利用原型鏈  instance1.__proto__ === Son.prototype ,而後手動修改 Son.prototype 的指向,使其指向一個  Father 的實例對象,從而實現繼承。
babel

  從上例中咱們能夠發現, instance1 自身並無 fatherValue、colors 這些屬性,可是咱們能夠經過原型鏈訪問獲得這些屬性,說明繼承成功。函數

 

  可是這裏就存在一個問題,咱們接着看下面的代碼:post

var instance1 = new Son();
console.log(instance1.colors);   // ["red", "yellow", "green"]
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "yellow", "green", "black"]


var instance2 = new Son();
console.log(instance2.colors);  // ["red", "yellow", "green", "black"]

  對象 instance1引用類型的 colors 修改,那麼其餘全部的實例中的 colors 都被修改了,這也是這種繼承方法的缺點之一:父類使用 this 聲明的屬性被全部實例共享。this

  另外,咱們也沒有辦法在實例子類的同時,根據須要向父類傳參數,不夠靈活url

 

2、借用構造函數(call)

function Father() {
  this.color = ['red', 'green', 'blue'];
}
function Son() {
  Father.call(this) // 核心,利用 this 在初始化的時候指向實例對象實現繼承
}
var instance1 = new Son();
instance1.color.push('black');
console.log(instance1.color); // 'red, green, blue, black'

var instance2 = new Son();
console.log(instance2.color); // 'red, green, blue'

附一張 instance1 內部結構圖:es5

 

  咱們能夠看到這裏與第一種方法的不一樣的地方在屬性的掛載上,第一種方法 colors 是掛在原型鏈上的,而這種方法 colors 直接是在子類的實例對象上的,因此咱們就能修正第一種方法的實例共享的問題。spa

  咱們仔細分析一下這裏面的核心關係:咱們在 new Son() 的時候,必定會執行函數調用語句  Father.call(this) 這句,而這一句實質是改變  Father 內部的 this 的指向,使其指向子實例對象,並在子實例對象上掛載  color 屬性(至關於 instance1.color = ['red', 'green', 'blue']; ),這樣,在 Father.call(this) 執行完以後,子實例對象上就會多一個屬性,而且,由於該過程當中執行的是函數調用,因此每次新實例化子對象的時候均會建立地址不一樣的 ['red', 'green', 'blue'] 並賦值,從而解決第一種方法中的共享問題。

 

  這種方法的優勢除解決共享問題外,還能夠在實例化子類型對象時向父類型傳遞參數。固然,也有缺點,由於這種方法與原型鏈沒有任何關係,故,子類只能繼承父類中經過  this 聲明(註冊)的屬性,不能訪問,也不能繼承父類  prototype 上的屬性/方法

父類方法沒法複用:由於沒法繼承父類的prototype,因此每次子類實例化都要執行父類函數,從新聲明父類this裏所定義的方法,所以方法沒法複用。 -- 這一句不知道怎麼理解

  

3、組合式繼承(call + new)

  這種方式是將上面兩種方法綜合一下:

function Father(name) {
  this.name = name;
  this.colors = ['red', 'green', 'blue'];
}

Father.prototype.sayFather = function() {
  console.log(this.name);
}

function Son(name, age) {
  // 繼承屬性
  // 第二次調用 Father();
  Father.call(this, name);
  this.age = age;
}

// 繼承方法 
// 第一次調用 Father();
Son.prototype = new Father();

// console.log(Son.prototype.constructor) // => Father() {}

// 重寫(扶正) Son.prototype 的 constructor, 讓其指向自身的構造函數 Son
// 由於上一句 (Son.prototype = new Father()) 的關係,如不修改,Son.prototype.constructor 會指向 Father
Son.prototype.constructor = Son;

Son.prototype.saySon = function() {
  console.log(this.age)
}

// console.log(Son.prototype.constructor) // () => Son() {}

var instance1 = new Son('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors);  // 'red,blue,green,black'
instance1.sayFather();  // 'Nicholas';
instance1.saySon();  // 29

var instance2 = new Son('Greg', 27);
console.log(instance2.colors);  // 'red,blue,green'
instance2.sayFather();  // 'Greg';
instance2.saySon();  // 27

  這樣的優勢天然是解決上面兩種方法的主要痛點。

 

  而後咱們來觀察下子實例的結構:

  緣由是在源碼中執行過兩次 Father(),這兩次分別在子實例的原型(Son.prototype)和子實例(new Father())上掛載了一樣的屬性,固然這裏不存在同一個子實例原型和子實例上的引用類型數據共享的問題(instance.colors !== instance.__proto__.colors)。

  而由於每一個子實例均會優先訪問自身的 1 中的屬性,因此這就繞過了父類使用 this 聲明的屬性被全部實例共享的問題

  固然,這也是這種方法的缺點:在使用子類建立實例對象時,其原型中會存在一份一樣的屬性/方法

 

  另外,還有幾點須要注意:

    1.凡是使用原型鏈(new Father())的方式,都會存在  Son.prototype.constructor 須要扶正的問題,因此第一種方式中須要補全。( ̄_, ̄ )

 

4、原型式繼承(Object.create())

function cloneObject(obj) {
  function F() {}
  F.prototype = obj;

  return new F();
}

var person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = cloneObject(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = cloneObject(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

console.log(person.friends);   // 'Shelby, Court, Van, Rob, Barbie'
console.log(anotherPerson.friends);  // 'Shelby, Court, Van, Rob, Barbie'
console.log(yetAnotherPerson.friends);  // 'Shelby, Court, Van, Rob, Barbie'

  其中, cloneObject 函數是  Object.create() 原生方法的模擬實現,這裏只是演示下,實際狀況中可使用 Object.create() 來代替。固然,這裏發生的複製都是淺複製。

 

  接着咱們看一會兒實例的內部結構:

、  和第一種方法打印出來的子實例結構相似,其實這兩種方式本質上是相同的,因此他們的名字就差一個字,缺點也同樣:共享引用類型;不能靈活傳參

 

5、寄生式繼承

  這種繼承方式僅在第四種方法的基礎上作了些許加強(能夠理解成定製化),修改不大:

function createAnother(original) {
  var clone = Object.create(original); 
  clone.sayHi = function() { // 加強的部分
    console.log('hi'); 
  }

  return clone;
}

var person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();  // 'hi'

  優缺點同原型式繼承。

 

6、寄生組合式繼承(call + 寄生式)

  結合借用構造函數和寄生模式實現繼承,是目前最成熟的方式,也是如今不少庫實現的方法。

function Father(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
Father.prototype.sayFather = function() {
  console.log(this.name);
}

function Son(name, age) {
  Father.call(this, name);
  this.age = age;
}

function inheritPrototype(son, father) {
  var prototype = Object.create(father.prototype); // 建立一個父類原型的副本對象
  prototype.constructor = son;   // 扶正 constructor, 不然會指向 father
  son.prototype = prototype;
}
inheritPrototype(Son, Father);

Son.prototype.saySon = function() {
  console.log(this.age);
}

var instance1 = new Son('xyc', 23);
var instance2 = new Son('lxy', 29);

instance1.colors.push('2'); // ['red', 'blue', 'green', '2']
instance2.colors.push('3'); // ['red', 'blue', 'green', '3']

  這種方式與組合式的區別在於使用  inheritPrototype 函數來代替  Son.prototype = new Father(); ,在整個過程當中只調用一次  Father() ,從而避免在  Son.prototype 上掛載多餘的屬性。咱們來看一會兒實例的結構:

 

  咱們能夠看到,在全部子實例的原型對象(instance.__proto__)上並無找到組合式存在的沒必要要的、重複屬性。因此總結來講,借用組合式實現參數傳遞,借用寄生式完善原型鏈創建,所以,還能正常使用  instanceof 和  isPrototypeOf()

 

7、ES 6 extends 繼承

  這個和 java 的用法一致,具體寫法就不貼了,就貼一下核心代碼實現,固然,是經過 babel 編譯成 es5 的:

function _inherits(subType, superType) {
  
    // 建立對象,建立父類原型的一個副本
    // 加強對象,彌補因重寫原型而失去的默認的constructor 屬性
    // 指定對象,將新建立的對象賦值給子類的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}

  其本質就是寄生組合的方式。

相關文章
相關標籤/搜索