徹底搞懂原型、原型鏈和繼承
- 2020 年 8 月 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__)
運行結果:
![]
本質:
使用原型鏈的混成模式實現對父類原型屬性和方法繼承,通過構造函數來實現對實例屬性的繼承
存在的問題:
是最理想的繼承方式。