【前端知識體系-JS相關】深刻理解JavaScript原型(繼承)和原型鏈

1. Javascript繼承

1.1 原型鏈繼承

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]
主要問題:javascript

  1. 引用類型的屬性被全部實例共享(this.children.push('name'))
  2. 在建立Child的實例的時候,不能向Parent傳參

1.2 借用構造函數(經典繼承)

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]
優勢:java

  1. 避免了引用類型的屬性被全部實例共享
  2. 能夠直接在Child中向Parent傳參

[!DANGER]
缺點:函數

  • 方法都在構造函數中定義了,每次建立實例都會建立一遍方法

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

/**
    * 父類構造函數
    * @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中最經常使用的繼承模式優化

2. 多種方式實現繼承及優缺點總結

2.1 原型式繼承

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

[!DANGER]
缺點:ui

  • 對於引用類型的屬性值始終都會共享相應的值,和原型鏈繼承同樣

2.2 寄生式繼承

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

[!DANGER]
缺點:與借用構造函數模式同樣,每次建立對象都會建立一遍方法this

2.3 寄生組合式繼承

2.3.1 基礎版本

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);

2.3.2 優化版本

// 自封裝一個繼承的方法
    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)

3. JS建立對象的方法

  • 字面量建立
  • 構造函數建立
  • 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)

4. 原型和原型鏈

4.1 原型

  1. JavaScript 的全部對象中都包含了一個 __proto__ 內部屬性,這個屬性所對應的就是該對象的原型
  2. JavaScript 的函數對象,除了原型 __proto__ 以外,還預置了 prototype 屬性
  3. 當函數對象做爲構造函數建立實例時,該 prototype 屬性值將被做爲實例對象的原型 __proto__

原型

4.2 原型鏈

  1. 任何一個實例對象經過原型鏈能夠找到它對應的原型對象,原型對象上面的實例和方法都是實例所共享的。es5

  2. 一個對象在查找以一個方法或屬性時,他會先在本身的對象上去找,找不到時,他會沿着原型鏈依次向上查找。prototype

[!NOTE]
注意: 函數纔有prototype,實例對象只有有__proto__, 而函數有的__proto__是由於函數是Function的實例對象指針

4.3 instanceof原理

[!NOTE]
判斷實例對象的__proto__屬性與構造函數的prototype是否是用一個引用。若是不是,他會沿着對象的__proto__向上查找的,直到頂端Object。code

4.4 判斷對象是哪一個類的直接實例

[!NOTE]
使用對象.construcor直接可判斷

4.5 構造函數,new時發生了什麼?

var obj  = {}; 
   obj.__proto__ = Base.prototype;
   Base.call(obj);
  1. 建立一個新的對象 obj;
  2. 將這個空對象的__proto__成員指向了Base函數對象prototype成員對象
  3. Base函數對象的this指針替換成obj, 至關於執行了Base.call(obj);
  4. 若是構造函數顯示的返回一個對象,那麼則這個實例爲這個返回的對象。 不然返回這個新建立的對象

4.6 類

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

// ES6
class Animal2 {
  constructor () {
    this.name = 'name';
  }
}
相關文章
相關標籤/搜索