1. 程式人生 > >【程式設計題與分析題】Javascript 之繼承的多種實現方式和優缺點總結

【程式設計題與分析題】Javascript 之繼承的多種實現方式和優缺點總結

[!NOTE]
能熟練掌握每種繼承方式的手寫實現,並知道該繼承實現方式的優缺點。

原型鏈繼承

    function Parent() {
      this.name = 'zhangsan';
      this.children = ['A', 'B', 'C'];
    }
    Parent.prototype.getName = function() {
      console.log(this.name);
    }
    
    function Child() {
      
    }
    Child.prototype = new Parent();
    var child = new Child();
    console.log(child.getName());

[!NOTE]
主要問題:
1. 引用型別的屬性被所有例項共享(this.children.push('name'))
2. 在建立Child的例項的時候,不能向Parent傳參

借用建構函式(經典繼承)

    function Parent(age) {
      this.names = ['zhangsan', 'lisi'];
      this.age = age;
      
      this.getName = function() {
        return this.names;
      }
      
      this.getAge = function() {
        return this.age;
      }
    }
    
    function Child(age) {
      Parent.call(this, age);
    }
    var child = new Child(18);
    child.names.push('haha');
    console.log(child.names);
    
    var child2 = new Child(20);
    child2.names.push('yaya');
    console.log(child2.names);

[!NOTE]
優點:
1. 避免了引用型別的屬性被所有例項共享
2. 可以直接在Child中向Parent傳參
缺點:
方法都在建構函式中定義了,每次建立例項都會建立一遍方法

組合繼承(原型鏈繼承和經典繼承雙劍合璧)

    /**
    * 父類建構函式
    * @param name
    * @constructor
    */
    function Parent(name) {
      this.name = name;
      this.colors = ['red', 'green', 'blue'];
    }
    
    Parent.prototype.getName = function() {
      console.log(this.name);
    }
    
    // child
    function Child(name, age) {
      Parent.call(this, name);
      this.age = age;
    }
    
    Child.prototype = new Parent();
    // 校正child的建構函式
    Child.prototype.constructor = Child;
    
    // 建立例項
    var child1 = new Child('zhangsan', 18);
    child1.colors.push('orange');
    console.log(child1.name, child1.age, child1.colors);    // zhangsan 18 (4) ["red", "green", "blue", "orange"]
    
    var child2 = new Child('lisi', 28);
    console.log(child2.name, child2.age, child2.colors);    // lisi 28 (3) ["red", "green", "blue"]

[!NOTE]
優點: 融合了原型鏈繼承和建構函式的優點,是Javascript中最常用的繼承模式

------ 高階繼承的實現

原型式繼承

    function createObj(o) {
      function F(){};
      // 關鍵:將傳入的物件作為建立物件的原型
      F.prototype = o;
      return new F();
    }
    
    // test
    var person = {
        name: 'zhangsan',
        friends: ['lisi', 'wangwu']
    }
    var person1 = createObj(person);
    var person2 = createObj(person);
    
    person1.name = 'wangdachui';
    console.log(person1.name, person2.name);  // wangdachui, zhangsan
    
    person1.friends.push('songxiaobao');
    console.log(person2.friends);       // lisi wangwu songxiaobao

[!WARNING]
缺點:
對於引用型別的屬性值始終都會共享相應的值,和原型鏈繼承一樣

寄生式繼承

    // 建立一個用於封裝繼承過程的函式,這個函式在內部以某種形式來增強物件
    function createObj(o) {
      var clone = Object.create(o);
      clone.sayName = function() {
        console.log('say HelloWorld');
      }
      return clone;
    }

[!WARNING]
缺點:與借用建構函式模式一樣,每次建立物件都會建立一遍方法

寄生組合式繼承

基礎版本

    function Parent(name) {
      this.name = name;
      this.colors = ['red', 'green', 'blue'];
    }
    
    Parent.prototype.getName = function() {
      console.log(this, name);
    }
    
    function Child(name, age) {
      Parent.call(this, name);
      this.age = age;
    }
    
    // test1:
    // 1. 設定子類例項的時候會呼叫父類的建構函式
    Child.prototype = new Parent();
    // 2. 建立子類例項的時候也會呼叫父類的建構函式
    var child1 = new Child('zhangsan', 18);   // Parent.call(this, name);
    
    
    // 思考:如何減少父類建構函式的呼叫次數呢?
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    
    // 思考:下面的這一句話可以嗎?
    /* 分析:因為此時Child.prototype和Parent.prototype此時指向的是同一個物件,
            因此部分資料相當於此時是共享的(引用)。
            比如此時增加 Child.prototype.testProp = 1; 
            同時會影響 Parent.prototype 的屬性的。
          如果不模擬,直接上 es5 的話應該是下面這樣吧
          Child.prototype = Object.create(Parent.prototype);*/
    Child.prototype = Parent.prototype;
    
    // 上面的三句話可以簡化為下面的一句話
    Child.prototype = Object.create(Parent.prototype);
    
    
    
    // test2:
    var child2 = new Child('lisi', 24);

優化版本

    // 自封裝一個繼承的方法
    function object(o) {
      // 下面的三句話實際上就是類似於:var o = Object.create(o.prototype)
      function F(){};
      F.prototype = o.prototype;
      return new F();
    }
    
    function prototype(child, parent) {
      var prototype = object(parent.prototype);
      // 維護原型物件prototype裡面的constructor屬性
      prototype.constructor = child;
      child.prototype = prototype;
    }
    
    // 呼叫的時候
    prototype(Child, Parent)

建立物件的方法

  • 字面量建立
  • 建構函式建立
  • Object.create()
var o1 = {name: 'value'};
var o2 = new Object({name: 'value'});

var M = function() {this.name = 'o3'};
var o3 = new M();

var P = {name: 'o4'};
var o4 = Object.create(P)

原型

  • JavaScript 的所有物件中都包含了一個 __proto__ 內部屬性,這個屬性所對應的就是該物件的原型
  • JavaScript 的函式物件,除了原型 __proto__ 之外,還預置了 prototype 屬性
  • 當函式物件作為建構函式建立例項時,該 prototype 屬性值將被作為例項物件的原型 __proto__

原型鏈

任何一個例項物件通過原型鏈可以找到它對應的原型物件,原型物件上面!

的例項和方法都是例項所共享的。

一個物件在查詢以一個方法或屬性時,他會先在自己的物件上去找,找不到時,他會沿著原型鏈依次向上查詢。

注意: 函式才有prototype,例項物件只有有__proto__, 而函式有的__proto__是因為函式是Function的例項物件

instanceof原理

判斷例項物件的__proto__屬性與建構函式的prototype是不是用一個引用。如果不是,他會沿著物件的__proto__向上查詢的,直到頂端Object。

判斷物件是哪個類的直接例項

使用物件.construcor直接可判斷

建構函式,new時發生了什麼?

   var obj  = {}; 
   obj.__proto__ = Base.prototype;
   Base.call(obj);  
  1. 建立一個新的物件 obj;
  2. 將這個空物件的__proto__成員指向了Base函式物件prototype成員物件
  3. Base函式物件的this指標替換成obj, 相當於執行了Base.call(obj);
  4. 如果建構函式顯示的返回一個物件,那麼則這個例項為這個返回的物件。 否則返回這個新建立的物件

類的宣告

// 普通寫法
function Animal() {
  this.name = 'name'
}

// ES6
class Animal2 {
  constructor () {
    this.name = 'name';
  }
}

繼承

借用建構函式法

在建構函式中 使用Parent.call(this)的方法繼承父類屬性。

原理: 將子類的this使用父類的建構函式跑一遍

缺點: Parent原型鏈上的屬性和方法並不會被子類繼承

function Parent() {
  this.name = 'parent'
}

function Child() {
  Parent.call(this);
  this.type = 'child'
}

原型鏈實現繼承

原理:把子類的prototype(原型物件)直接設定為父類的例項

缺點:因為子類只進行一次原型更改,所以子類的所有例項儲存的是同一個父類的值。
當子類物件上進行值修改時,如果是修改的原始型別的值,那麼會在例項上新建這樣一個值;
但如果是引用型別的話,他就會去修改子類上唯一一個父類例項裡面的這個引用型別,這會影響所有子類例項

function Parent() {
  this.name = 'parent'
  this.arr = [1,2,3]
}

function Child() {
  this.type = 'child'
}

Child.prototype = new Parent();
var c1 = new Child();
var c2 = new Child();
c1.__proto__ === c2.__proto__

組合繼承方式

組合建構函式中使用call繼承和原型鏈繼承。

原理: 子類建構函式中使用Parent.call(this);的方式可以繼承寫在父類建構函式中this上繫結的各屬性和方法;
使用Child.prototype = new Parent()的方式可以繼承掛在在父類原型上的各屬性和方法

缺點: 父類建構函式在子類建構函式中執行了一次,在子類繫結原型時又執行了一次

function Parent() {
  this.name = 'parent'
  this.arr = [1,2,3]
}

function Child() {
  Parent.call(this);
  this.type = 'child'
}

Child.prototype = new Parent();

組合繼承方式 優化1:

因為這時父類建構函式的方法已經被執行過了,只需要關心原型鏈上的屬性和方法了

Child.prototype = Parent.prototype;

缺點:

  • 因為原型上有一個屬性為constructor,此時直接使用父類的prototype的話那麼會導致 例項的constructor為Parent,即不能區分這個例項物件是Child的例項還是父類的例項物件。
  • 子類不可直接在prototype上新增屬性和方法,因為會影響父類的原型

注意:這個時候instanseof是可以判斷出例項為Child的例項的,因為instanceof的原理是沿著物件的__proto__判斷是否有一個原型是等於該建構函式的原型的。這裡把Child的原型直接設定為了父類的原型,那麼: 例項.__proto__ === Child.prototype === Child.prototype

組合繼承方式 優化2 - 新增中間物件【最通用版本】:

function Parent() {
  this.name = 'parent'
  this.arr = [1,2,3]
}

function Child() {
  Parent.call(this);
  this.type = 'child'
}

Child.prototype = Object.create(Parent.prototype); //提供__proto__
Child.prototype.constrctor = Child;

Object.create()方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__

建立JS物件的多種方式總結

工廠模式

 
    /**
    * 工廠模式建立物件
    * @param name
    * @return {Object}
    */
    function createPerson(name){
        var o = new Object();
        o.name = name;
        o.getName = function() {
          console.log(this.name);
        }
        return o;
    }
    var person = createPerson('zhangsan');
    console.log(person.__proto__ === Object.prototype); // true

缺點:無法識別當前的物件,因為建立的所有物件例項都指向的是同一個原型

建構函式模式

建構函式建立物件基礎版本

    /**
    * 使用建構函式的方式來建立物件
    * @param name
    * @constructor
    */
    function Person(name) {
      this.name = name;
      this.getName = function() {
        console.log(this.name)
      }
    }
    var person = new Person('lisi');
    console.log(person.__proto__ === Person.prototype)

優點:例項剋識別偽一個特定的型別
缺點:每次建立例項物件的時候,每個方法都會被建立一次

建構函式模式優化

    function Person(name) {
      this.name = name;
      this.getName = getName;
    }
    
    function getName() {
      console.log(this.name);
    }
    
    var person = new Person('zhangsan');
    console.log(person.__proto__ === Person.prototype);

優點:解決了每個方法都要被重新建立的問題
缺點:不合乎程式碼規範……

原型模式

原型模式基礎版

    function Person(name) {
      
    }
    Person.prototype.name = 'lisi';
    Person.prototype.getName = function() {
      console.log(this.name);
    }
    var person = new Person();
    console.log(Person.prototype.constructor)       // Person

優點:方法不會被重新建立
缺點:1. 所有的屬性和方法所有的例項上面都是共享的;2. 不能初始化引數

原型模式優化版本一

    function Person(name) {
      
    }
    Person.prototype = {
        name: 'lisi',
        getName: function() {
          console.log(this.name);
        }
    }
    var person = new Person();
    console.log(Person.prototype.constructor)       // Object
    console.log(person.constructor == person.__proto__.constructor) // true

優點:封裝性好了一些
缺點:重寫了Person的原型prototype屬性,丟失了原始的prototype上的constructor屬性

原型模式優化版本二

    function Person(name) {
      
    }
    Person.prototype = {
        constructor: Person,
        name: 'lisi',
        getName: function() {
          console.log(this.name)
        }
    }
    var person = new Person();

優點:例項可以通過constructor屬性找到所屬的建構函式
缺點:所有的屬性和方法都共享,而且不能初始化引數

組合模式

    function Person(name) {
      this.name = name;
    }
    Person.prototype = {
        constructor: Person,
        getName: function() {
          console.log(this.name)
        }
    }
    var person = new Person('zhangsan');

優點:基本符合預期,屬性私有,方法共享,是目前使用最廣泛的方式
缺點:方法和屬性沒有寫在一起,封裝性不是太好

動態原型模式

    // 第一種建立思路:
    function Person(name) {
       this.name = name;
       if (typeof this.getName !== 'function') {
           Person.prototype.getName = function() {
             console.log(this.name);
           }
       }
    }
    var person = new Person();

    // 第二種建立的思路:使用物件字面量重寫原型上的方法
    function Person(name) {
      this.name = name;
      if (typeof this.getName !== 'function') {
          Person.prototype = {
              constructor: Person,
              getName: function() {
                console.log(this.name)
              }
          }
          return new Person(name);
      }
    }
    
    var person1 = new Person('zhangsan');
    var person2 = new Person('lisi');
    console.log(person1.getName());
    console.log(person2.getName());
    

寄生建構函式模式

    /**
    * 寄生建構函式模式
    * @param name
    * @return {Object}
    * @constructor
    */
   function Person(name){
        var o = new Object();
        o.name = name;
        o.getName = function() {
          console.log(this.name)
        }
        return o;
   }
   var person = new Person('zhangsan');
   console.log(person instanceof Person);   // false
   console.log(person instanceof Object);   // true
   
   
   // 使用寄生-建構函式-模式來建立一個自定義的陣列
   /**
    * 特殊陣列的構造器
    * @constructor
    */
   function SpecialArray() {
     var values = new Array();
     /*for (var i = 0, len = arguments.length; i < len; i++) {
         values.push(arguments[i]);
     }*/
     // 開始新增資料(可以直接使用apply的方式來優化程式碼)
     values.push.apply(values, arguments);
     
     // 新增的方法
     values.toPipedString = function(){
         return this.join('|');
     }
     
     return values;
   }
   
   // 使用new來建立物件
   var colors1 = new SpecialArray('red1', 'green1', 'blue1');
   // 不使用new來建立物件
   var colors2 = SpecialArray('red2', 'green2', 'blue2');
   
   console.log(colors1, colors1.toPipedString());
   console.log(colors2, colors2.toPipedString());

穩妥建構函式模式

    /**
    * 穩妥的建立物件的方式
    * @param name
    * @return {number}
    * @constructor
    */
    function Person(name){
        var o = new Object();
        o.sayName = function() {
           // 這裡有點類似於在一個函式裡面使用外部的變數
           // 這裡直接輸出的是name
          console.log(name);
        }
        return o;
    }
    var person =  Person('lisi');
    person.sayName();
    person.name = 'zhangsan';
    person.sayName();
    console.log(person instanceof Person);      // false
    console.log(person instanceof Object);      // false

[!NOTE]
與寄生的模式的不同點:1. 新建立的例項方法不引用this 2.不使用new操作符呼叫建構函式
優點:最適合一些安全的環境中使用
缺點:和工廠模式一樣,是無法識別物件的所屬型別的