js繼承、構造函數繼承、原型鏈繼承、組合繼承、組合繼承優化、寄生組合繼承

2018.06.03javascript

第一部分:導入

一、構造函數的屬性

funcion A(name) {
    this.name = name; // 實例基本屬性 (該屬性,強調私有,不共享)
    this.arr = [1]; // 實例引用屬性 (該屬性,強調私用,不共享)
    this.say = function() { // 實例引用屬性 (該屬性,強調複用,須要共享)
        console.log('hello')
    }
}
注意:數組和方法都屬於‘實例引用屬性’,可是數組強調私有、不共享的。方法須要複用、共享。

注意:在構造函數中,通常不多有數組形式的引用屬性,大部分狀況都是:基本屬性 + 方法。

二、原型對象的做用

原型對象的用途是爲每一個實例對象存儲共享的方法和屬性,它僅僅是一個普通對象而已。而且全部的實例是共享同一個原型對象,所以有別於實例方法或屬性,原型對象僅有一份。而實例有不少份,且實例屬性和方法是獨立的。

在構造函數中:爲了屬性(實例基本屬性)的私有性、以及方法(實例引用屬性)的複用、共享。咱們提倡:html

  • 將屬性封裝在構造函數中
  • 將方法定義在原型對象上
funcion A(name) {
    this.name = name; // (該屬性,強調私有,不共享)
}
A.prototype.say = function() { // 定義在原型對象上的方法 (強調複用,須要共享)
        console.log('hello')
}

// 不推薦的寫法:[緣由](https://blog.csdn.net/kkkkkxiaofei/article/details/46474303)
A.prototype = {
    say: function() { 
        console.log('hello')
    }
}

第二部分:js 繼承---各類方式的優缺點

方式一、原型鏈繼承

  • 核心:將父類實例做爲子類原型
  • 優勢:方法複用java

    • 因爲方法定義在父類的原型上,複用了父類構造函數的方法。好比say方法。
  • 缺點:數組

    • 建立子類實例的時候,不能傳參數。
    • 子類實例共享了父類構造函數的引用屬性,好比arr屬性。
function Parent() {
    this.name = '父親'; // 實例基本屬性 (該屬性,強調私有,不共享)
    this.arr = [1]; // (該屬性,強調私有)
}
Parent.prototype.say = function() { // -- 將須要複用、共享的方法定義在父類原型上 
    console.log('hello')
}
function Child(like) {
    this.like = like;
}
Child.prototype = new Parent() // 核心

let boy1 = new Child()
let boy2 = new Child()

// 優勢:共享了父類構造函數的say方法
console.log(boy1.say(), boy2.say(), boy1.say === boy2.say); // hello , hello , true

// 缺點1:不能傳參數
// 缺點2:
console.log(boy1.name, boy2.name, boy1.name===boy2.name); // 父親,父親,true

boy1.arr.push(2); // 修改了boy1的arr屬性,boy2的arr屬性,也會變化,由於兩個實例的原型上(Child.prototype)有了父類構造函數的實例屬性arr;因此只要修改了boy1.arr,boy2.arr的屬性也會變化。  ----  原型上的arr屬性是共享的。
console.log(boy2.arr); // [1,2]

注意:修改boy1的name屬性,是不會影響到boy2.name。由於name是基本屬性,不是引用屬性。

方式二、借用構造函數

  • 核心:借用父類的構造函數來加強子類實例,等因而複製父類的實例屬性給子類。
  • 優勢:實例之間獨立。app

    • 建立子類實例,能夠向父類構造函數傳參數。
    • 子類實例不共享父類構造函數的引用屬性。如arr屬性
  • 缺點:函數

    • 父類的方法不能複用
    因爲方法在父構造函數中定義,致使方法不能複用(由於每次建立子類實例都要建立一遍方法)。好比say方法。(方法應該要複用、共享)
    • 子類實例,繼承不了父類原型上的屬性。(由於沒有用到原型)
function Parent(name) {
    this.name = name; // 實例基本屬性 (該屬性,強調私有,不共享)
     this.arr = [1]; // (該屬性,強調私有)
    this.say = function() { // 實例引用屬性 (該屬性,強調複用,須要共享)
        console.log('hello')
    }
}
function Child(name,like) {
    Parent.call(this,name);  // 核心
    this.like = like;
}
let boy1 = new Child('小紅','apple');
let boy2 = new Child('小明', 'orange ');

// 優勢1:可傳參
console.log(boy1.name, boy2.name); // 小紅, 小明

// 優勢2:不共享父類構造函數的引用屬性
boy1.arr.push(2);
console.log(boy1.arr,boy2.arr);// [1,2] [1]

// 缺點1:方法不能複用
console.log(boy1.say === boy2.say) // false (說明,boy1和boy2 
的say方法是獨立,不是共享的)

// 缺點2:不能繼承父類原型上的方法
Parent.prototype.walk = function () {   // 在父類的原型對象上定義一個walk方法。
    console.log('我會走路')
}
boy1.walk;  // undefined (說明實例,不能得到父類原型上的方法)

方式三、組合繼承

  • 核心:經過調用父類構造函數,繼承父類的屬性並保留傳參的優勢;而後經過將父類實例做爲子類原型,實現函數複用。
  • 優勢:優化

    • 保留構造函數的優勢:建立子類實例,能夠向父類構造函數傳參數。
    • 保留原型鏈的優勢:父類的實例方法定義在父類的原型對象上,能夠實現方法複用。
    • 不共享父類的引用屬性。好比arr屬性
  • 缺點:this

    • 因爲調用了2次父類的構造方法,會存在一份多餘的父類實例屬性,具體緣由見文末。
  • 注意:'組合繼承'這種方式,要記得修復Child.prototype.constructor指向
第一次Parent.call(this);從父類拷貝一份父類實例屬性,做爲子類的實例屬性,

第二次Child.prototype = new Parent();建立父類實例做爲子類原型,此時這個父類實例就又有了一份實例屬性,但這份會被第一次拷貝來的實例屬性屏蔽掉,因此多餘。.net

爲啥是兩次?若是仍是,不清楚,能夠看文末,我會詳細講解!prototype

function Parent(name) {
    this.name = name; // 實例基本屬性 (該屬性,強調私有,不共享)
    this.arr = [1]; // (該屬性,強調私有)
}
Parent.prototype.say = function() { // --- 將須要複用、共享的方法定義在父類原型上 
    console.log('hello')
}
function Child(name,like) {
    Parent.call(this,name,like) // 核心   第二次
    this.like = like;
}
Child.prototype = new Parent() // 核心   第一次

<!--這裏是修復構造函數指向的代碼-->

let boy1 = new Child('小紅','apple')
let boy2 = new Child('小明','orange')

// 優勢1:能夠傳參數
console.log(boy1.name,boy1.like); // 小紅,apple

// 優勢2:可複用父類原型上的方法
console.log(boy1.say === boy2.say) // true

// 優勢3:不共享父類的引用屬性,如arr屬性
boy1.arr.push(2)
console.log(boy1.arr,boy2.arr); // [1,2] [1] 能夠看出沒有共享arr屬性。

注意:爲啥要修復構造函數的指向?
console.log(boy1.constructor); // Parent 你會發現實例的構造函數竟然是Parent。
而實際上,咱們但願子類實例的構造函數是Child,因此要記得修復構造函數指向。修復以下
Child.prototype.constructor = Child;
其實Child.prototype = new Parent()

console.log(Child.prototype.__proto__ === Parten.prototype); // true

方式四、組合繼承優化1

  • 核心:
經過這種方式,砍掉父類的實例屬性,這樣在調用父類的構造函數的時候,就不會初始化兩次實例,避免組合繼承的缺點。
  • 優勢:

    • 只調用一次父類構造函數。
    • 保留構造函數的優勢:建立子類實例,能夠向父類構造函數傳參數。
    • 保留原型鏈的優勢:父類的實例方法定義在父類的原型對象上,能夠實現方法複用。
  • 缺點:

    • 修正構造函數的指向以後,父類實例的構造函數指向,同時也發生變化(這是咱們不但願的)
  • 注意:'組合繼承優化1'這種方式,要記得修復Child.prototype.constructor指向
緣由是:不能判斷子類實例的直接構造函數,究竟是子類構造函數仍是父類構造函數。
function Parent(name) {
    this.name = name; // 實例基本屬性 (該屬性,強調私有,不共享)
    this.arr = [1]; // (該屬性,強調私有)
}
Parent.prototype.say = function() { // --- 將須要複用、共享的方法定義在父類原型上 
    console.log('hello')
}
function Child(name,like) {
    Parent.call(this,name,like) // 核心  
    this.like = like;
}
Child.prototype = Parent.prototype // 核心  子類原型和父類原型,實質上是同一個

<!--這裏是修復構造函數指向的代碼-->

let boy1 = new Child('小紅','apple')
let boy2 = new Child('小明','orange')
let p1 = new Parent('小爸爸')

// 優勢1:能夠傳參數
console.log(boy1.name,boy1.like); // 小紅,apple
// 優勢2:
console.log(boy1.say === boy2.say) // true

// 缺點1:當修復子類構造函數的指向後,父類實例的構造函數指向也會跟着變了。
具體緣由:由於是經過原型來實現繼承的,Child.prototype的上面是沒有constructor屬性的,就會往上找,這樣就找到了Parent.prototype上面的constructor屬性;當你修改了子類實例的construtor屬性,全部的constructor的指向都會發生變化。

沒修復以前:console.log(boy1.constructor); // Parent
修復代碼:Child.prototype.constructor = Child
修復以後:console.log(boy1.constructor); // Child
          console.log(p1.constructor);// Child 這裏就是存在的問題(咱們但願是Parent)

方式五、組合繼承優化2 又稱 寄生組合繼承 --- 完美方式

  • 核心:
  • 優勢:完美i
  • 缺點:---
function Parent(name) {
    this.name = name; // 實例基本屬性 (該屬性,強調私有,不共享)
    this.arr = [1]; // (該屬性,強調私有)
}
Parent.prototype.say = function() { // --- 將須要複用、共享的方法定義在父類原型上 
    console.log('hello')
}
function Child(name,like) {
    Parent.call(this,name,like) // 核心  
    this.like = like;
}
Child.prototype = Object.create(Parent.prototype) // 核心  經過建立中間對象,子類原型和父類原型,就會隔離開。不是同一個啦,有效避免了方式4的缺點。

<!--這裏是修復構造函數指向的代碼-->
Child.prototype.constructor = Child

let boy1 = new Child('小紅','apple')
let boy2 = new Child('小明','orange')
let p1 = new Parent('小爸爸')


注意:這種方法也要修復構造函數的
修復代碼:Child.prototype.constructor = Child
修復以後:console.log(boy1.constructor); // Child
          console.log(p1.constructor);// Parent  完美

第三部分:其餘 + 相關問題解答

一、Object.create() 或 Object.create(object, [,propertiesObject])

Object.create() 的第二參數,是可選的。
- Object.create() 的內部原理:
// 其中,o 是新建立對象的原型(對象)
function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}
注意:以前,Object.create()沒有出現以前,就是採用的這種方式。
參見《js高級程序設計》P170
  • Object.create() 作了哪幾件事情?
  1. 建立空對象{}
  2. 指定空對象{}的原型爲Object.create()的參數。
  • new 與 Object.create() 的區別?
如下是個人我的看法,(若有不對,還請指正):

new 產生的實例,優先獲取構造函數上的屬性;構造函數上沒有對應的屬性,纔會去原型上查找;若是構造函數中以及原型中都沒有對應的屬性,就會報錯。

Object.create() 產生的對象,只會在原型上進行查找屬性,原型上沒有對應的屬性,就會報錯。

let Base1 = function() {
  this.a = 1
}
let o1 = new Base1()
let o2 = Object.create(Base1.prototype)
console.log(o1.a); // 1
console.log(o2.a); // undefined



let Base2 = function() {}
Base2.prototype.a = 'aa'
let o3 = new Base2()
let o4 = Object.create(Base2.prototype)
console.log(o3.a); // aa
console.log(o4.a); // aa



let Base3 = function() {
  this.a = 1
}
Base3.prototype.a = 'aa'
let o5 = new Base3()
let o6 = Object.create(Base3.prototype)
console.log(o5.a); // 1
console.log(o6.a); // aa

二、new 的過程

funciton Func(name) {
    this.name = name
}
let p = new Func('小紅')

new 的過程,作了啥?作了四件事。

  • 建立一個空對象obj:let obj = new Object()
  • 設置原型鏈
obj.__proto__ = Func.prototype
就是:將新對象的__proto__ 指向構造函數的prototype
  • 將構造函數Func的this指向obj,並執行構造函數Func
let result = Func.call(obj)
就是:使用call或apply,將構造函數的this綁定到新對象,並執行構造函數
  • 判斷構造函數Func的返回值類型
若是是引用類型,就返回這個引用類型的對象。若是是值類型或沒有return,則返回空對象obj。
if (typeof(result) === "object"){  
  func=result;  
}  
else{  
   func=obj; // 默認返回
}
注意:js中的構造函數,是不須要有返回值的,因此默認返回的是新建立的空對象obj

三、爲啥‘組合繼承’這種方式,會執行兩次父類構造函數??

  • 第一次:Child.prototype = new Parent()
‘new 的過程’的第三步,其實就是執行了父類構造函數。
  • 第二次:Parent.call(this,name,like)
call的做用是改變函數執行時的上下文。好比:A.call(B)。其實,最終執行的仍是A函數,只不過是用B來調用而已。因此,你就懂了Parent.call(this,name,like) ,也就是執行了父類構造函數。

第四部分:參考連接

相關文章
相關標籤/搜索