徹底搞懂原型、原型鏈和繼承

享碼yy發表於2020-08-25

一、為什麼有了原型?

?️從建構函式模式到原型模式

1、?建構函式模式

建構函式可用來建立特定型別的物件,可以建立自定義的建構函式來定義自定義物件型別的屬性和方法
如下程式碼:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    console.log(this.name);
  }
}
const person1 = new Person('LiLi', 25);
const person2 = new Person('Bob', 26);

通過建構函式建立了自定義物件person1 person2,分別有自己的屬性和方法,但是這種建立物件的方式有一個問題,就是在每個Person例項中都要重新建立sayName方法,如下,輸出是false可以證明

console.log(person1.sayName === person2.sayName)


這是因為函式也是物件,每定義一個函式也就是例項化一個物件,即下面兩段程式碼是等價的。所以不同的例項物件具有了不同的作用域鏈。當然,也可以把sayName函式的定義定義到建構函式外面,可以解決上面的問題,但是這種方式不具有封裝性,不利於程式碼的維護。由此引出了原型模式。

this.sayName = function () {
    console.log(this.name);
  }
}
this.sayName = new Function('console.log(this.name)')

2、?原型模式

首先,每個函式都有一個prototype(原型)屬性,當然建構函式中也有原型屬性,這個屬性是一個指標,指向一個物件,而這個物件中包含了特定型別的所有例項共享的屬性和方法。也就是prototype就是呼叫建構函式而建立的例項物件的原型物件。使用原型物件的好處是可以讓所有的例項物件共享它所包含的屬性和方法。換句話說,不必在建構函式中定義例項物件的資訊,而是可以將這些資訊直接新增到原型物件中。

function Person() {}
Person.prototype.name = 'LiLi';
Person.prototype.age = 25;
Person.prototype.sayName = function () {
  console.log(this.name);
};
const person1 = new Person();
const person2 = new Person();
console.log(person1.sayName === person2.sayName)

結果為true,說明所有例項訪問的都是同一組屬性和同一個sayName()函式

?理解原型物件

1、無論什麼時候,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個prototype屬性,這個屬性指向的就是原型物件。所以建構函式中天然的帶有一個指標prototype,指向一個物件,我們就把這個物件叫做原型物件
以上面的為例來看一下Person.prototype具體是什麼?

console.log(Person.prototype)
console.log(Person.prototype.constructor)


2、建構函式一旦建立,它的原型物件會自動獲取一個constructor(建構函式)屬性,這個屬性是一個指向prototype屬性所在函式的指標。比如上面的例子:Person.prototype.constructor=Person

?注意:建構函式剛建立的時候,原型物件中只有constructor屬性,裡面其它的方法都是從Object繼承而來的

3、當呼叫建構函式建立一個新例項後,該例項的內部將包含一個指標__proto__,指向建構函式的原型物件。

用圖來直觀的感受下建構函式、原型物件和例項三者之間的關係:

到此,原型鏈的概念也就引出來了,任何物件內部都有一個指標__proto__,指向建構函式的原型物件,通過這個__proto__屬性連起來的原型物件就叫原型鏈,原型鏈的盡頭是建構函式Object原型物件的__proto__,為null。
這也是查詢物件中屬性和方法的查詢機制,搜尋首先從物件例項開始,如果沒有找到,則繼續搜尋__proto__指標指向的原型物件,依次在原型鏈上查詢,直到找到為止,或者查詢到null為止。
因為這個查詢機制,物件例項是不能改變原型物件中的值,因為搜尋的時候就直接查詢到例項中的屬性,相當於遮蔽了原型物件中儲存的同名屬性。

二、繼承

1、?原型鏈實現繼承

?思想:

利用例項和原型物件之間的關係(如果不清楚繼續返回去看上一節),讓一個引用型別繼承另一個引用型別的屬性和方法,即把子類的 prototype(原型物件)直接設定為父類的例項。

function Parent() {
  this.name = "parent";
  this.arr = [1, 2, 3];
}
Parent.prototype.getName = function () {
  return this.name;
}

function Son() {
  this.type = "child";
}

Son.prototype = new Parent();
Son.prototype.getType = function () {
  return this.type;
}
const s1 = new Son();
const s2 = new Son();

?本質:

重寫子類的原型物件,替換成父類的例項。也就是原來存在於父類Parent例項中的屬性和方法,現在也存在於子類的原型物件Son.prototype中了。
以下程式碼的執行結果印證了這一點:

console.log(s1.__proto__)
console.log(s1.__proto__.__proto__)
console.log(s1.__proto__.constructor)

?存在的問題:

1、當父類的建構函式中定義的例項屬性會作為子類原型中的屬性,所以子類所有的例項物件都會共享這一個屬性,當子類例項物件上進行值修改時,如果是修改的原始型別的值,那麼會在例項上新建這樣一個值;但如果是引用型別的話,它就會去修改子類上唯一一個父類例項裡面的這個引用型別,這會影響所有子類例項。
2、在建立子型別的例項時,不能向父型別的建構函式中傳遞引數。

2、?借用建構函式法實現繼承

?思想:

在子類建構函式的內部呼叫父類的建構函式。

function Parent(_name) {
  this.name = _name;
  this.arr = [1, 2, 3];
}
Parent.prototype.getName = function () {
  return this.name;
}

function Son(_name) {
  //繼承了Parent 同時還傳遞了引數
  Parent.call(this, _name)
}

Son.prototype.getType = function () {
  return this.type;
}
const s1 = new Son();
s1.arr.push(4)
console.log(s1.arr)
const s2 = new Son('Bob');
console.log(s2.arr)
console.log(s2.name)

?執行結果:


從執行結果清楚的看到,建構函式法完美解決了原型鏈繼承中存在的兩個問題

?本質:

函式不過是在特定環境中執行程式碼的物件,因此通過apply()和call()方法可以在將來心建立的物件上執行建構函式。

?存在的問題:

父類原型鏈上的屬性和方法並不會被子類繼承

console.log(s2.__proto__)
console.log(s2.__proto__.__proto__)
console.log(s2.getName)

3、?組合繼承

?思想:

將原型鏈和借用建構函式的技術組合到一起

function Parent(_name) {
  this.name = _name;
  this.arr = [1, 2, 3];
}
Parent.prototype.getName = function () {
  return this.name;
}

function Son(_name) {
  Parent.call(this, _name)
}
Son.prototype = new Parent();
//Son.prototype = new Parent();導致Son.prototype.constructor指向改變 所以要改回來
Son.prototype.constructor = Son;
Son.prototype.getType = function () {
  return this.type;
}
const s1 = new Son();
s1.arr.push(4)
const s2 = new Son('Bob');
console.log(s2.name)
console.log(s2.arr)
console.log(s2.__proto__)
console.log(s2.__proto__.__proto__)
console.log(s2.getName())

?執行結果:

?本質:

使用原型鏈實現對父類原型屬性和方法繼承,通過建構函式來實現對例項屬性的繼承

?存在的問題:

無論在什麼情況下,都會呼叫兩次父類建構函式:一次是在建立子型別原型的時候,另一次是在子型別建構函式內部

4、?寄生組合式繼承

?思想:

不需要為了子類的原型而呼叫父類的建構函式,只需要父類的__proto__提供查詢組成原型鏈即可

function Parent(_name) {
  this.name = _name;
  this.arr = [1, 2, 3];
}
Parent.prototype.getName = function () {
  return this.name;
}

function Son(_name) {
  //繼承了Parent 同時還傳遞了引數
  Parent.call(this, _name)
}
//提供__proto__就可以了
// 不用這種形式Son.prototype = Parent.prototype; 是因為子類
// 不可直接在 prototype 上新增屬性和方法,因為會影響父類的原型
const pro = Object.create(Parent.prototype) // pro.__proto__即Parent.prototype

pro.constructor = Son
Son.prototype = pro

Son.prototype.getType = function () {
  return this.type;
}
const s1 = new Son();
s1.arr.push(4)
const s2 = new Son('Bob');
console.log(s2.name)
console.log(s2.arr)
console.log(s2.__proto__)
console.log(s2.__proto__.__proto__)

?執行結果:

![]

?本質:

使用原型鏈的混成模式實現對父類原型屬性和方法繼承,通過建構函式來實現對例項屬性的繼承

?存在的問題:

是最理想的繼承方式。

相關文章