目錄
- 原型鏈與繼承
- new 關鍵字的執行程序
- 建構式、實體物件和原型物件
- 原型鏈的概念及圖解
- 第一層
__proto__指向:實體物件 - 第二層
__proto__指向:Function.prototype和Foo.prototype - 第三層
__proto__指向:Object.prototype)
- 第一層
- 原型鏈繼承
- 盜用建構式
- 組合繼承( = 原型鏈繼承 + 盜用建構式 )
- 原型繼承
- 寄生繼承
- 寄生組合繼承( = 組合繼承 + 原型繼承 + 寄生繼承 )
- class繼承(ES6 語法)( ≈ 寄生組合繼承 )
原型鏈與繼承
new 關鍵字的執行程序
讓我們回顧一下,this 指向里提到的new關鍵字執行程序,
- 創建一個新的空物件
- 將建構式的原型賦給新創建物件(實體)的隱式原型
- 利用顯式系結將建構式的 this 系結到新創建物件并為其添加屬性
- 回傳這個物件
手寫new關鍵字的執行程序:
function myNew(fn, ...args) { // 建構式作為引數
let obj = {}
obj.__proto__ = fn.prototype
fn.apply(obj, args)
return obj
}
這里提到了__proto__ 和prototype:前者被稱為隱式原型,后者被稱為顯式原型,
建構式、實體物件和原型物件
三者的概念
建構式:用于生成實體物件,建構式可分為兩類:
- 自定義建構式:
function foo () {} - 原生建構式:
function Function () {}和function Object () {}等
原型物件:每個建構式都有自己的原型物件,可通過prototype訪問,
實體物件:可由建構式通過new關鍵字生成的物件,
三者的關系
建構式可以通過prototype訪問其原型物件,而原型物件可通過constructor訪問其建構式,建構式可通過new關鍵字創建實體物件,實體物件可通過__proto__ 訪問其原型物件,
我們來看一段代碼的輸出結果:
function Foo(name, age) {
this.name = name
this.age = age
}
let a = new Foo('小明', 22)
console.log('建構式:', Foo)
console.log('原型物件', Foo.prototype)
console.log('實體物件', a)
// 可以輸出一下,看看它們都是什么樣子
可以看出實體物件內部的第一個[[Prototype]]的展開內容等于原型物件的展開內容,可構建一個等式如下:
// 實體物件可通過 __proto__ 訪問其原型物件
a.__proto__ === Foo.prototype // true
// 原型物件可通過 constructor 訪問其建構式
Foo.prototype.constructor === Foo // true
原型鏈的概念及圖解
來看一張關于原型鏈的經典圖:
上面這張圖的箭頭乍一看能讓人頭疼,我們對圖中的元素進行分類并劃分層次,可有以下三層:
第一層__proto__指向:實體物件
- 通過建構式生成的實體物件
// 生成實體物件
function Foo() {}
let obj1 = new Foo()
// __proto__指向驗證
obj1.__proto__ === Foo.prototype // true
- 通過
new Object()、物件字面量生成的實體物件
// 生成實體物件
let obj2 = new Object()
// __proto__指向驗證
obj2.__proto__ === Object.prototype // true
- 通過
function或class宣告生成的實體物件
// 生成實體物件
function Foo(){}
// 原生建構式
// function Function(){}
// function Object(){}
// __proto__指向驗證
Foo.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
說明:其實我們自己定義的函式也是由Function建構式生成的實體物件,
第二層__proto__指向:Function.prototype和Foo.prototype
Foo.prototype.__proto__ === Object.prototype // true
Function.prototype.__proto__ === Object.prototype // true
第三層__proto__指向:Object.prototype)
Object.prototype.__proto__ === null // true
我們自己再畫一張圖看一下:
自底向上有三層的__proto__構成基本的JavaScript原型模式生態,最后再總結一下規則:
- 實體物件都會指向其建構式原型
- 建構式原型都會指向
Object.prototype Object.prototype最終指向null
總結:其實我們的原型鏈指的就是__proto__的路徑,
注意:這里只是為了原型鏈能更加直觀,請不要忘了建構式原型的constructor屬性,它會指回對應的建構式,
原型鏈繼承
我們利用任務驅動型的方法去學習繼承方式,考慮這樣一個類結構:
- 普通用戶:作為父類
- VIP 用戶:作為子類
說明:VIP用戶需要繼承普通用戶,其中,VIP用戶的武器串列可以添加屠龍寶刀,
原型鏈搜索機制:若要訪問當前物件所沒有的屬性和方法,則會首先以當前物件為起點沿著原型鏈__proto__向上尋找每個物件內部的屬性和方法,直到找到對應的屬性和方法,沒有則會直接走到原型鏈盡頭null,
來看這樣一段代碼:
function USER(username, password) {
this.username = username
this.password = password
this.weapon = ['水果小刀']
}
VIP.prototype = new USER() // 為什么要放到中間?
// 注意:改寫原型,要記得把 constructor 指會原建構式
VIP.prototype.constructor = VIP
function VIP() { }
VIP.prototype.addWeapon = function (weaponName) {
this.weapon.push(weaponName)
}
let a = new VIP('小明')
// 缺陷1:無法給父類建構式傳參,只能在 VIP 中自行添加相應引數,無法實作父類屬性重用
let b = new VIP()
b.addWeapon('屠龍寶刀')
console.log(b.weapon)
let c = new VIP()
console.log(c.weapon)
// 缺陷2: 我們想要單獨給實體 b 的武器串列添加一把屠龍寶刀,結果是實體 c 的武器串列也會增加屠龍寶刀
原型鏈__proto__實作繼承會經過的物件(從子類實體到父類原型):
-
子類建構式實體 :
new VIP() -
父類建構式實體 :
new USER() -
父類建構式原型 :
USER.prototype
我們可以構建兩個運算式去驗證:
new VIP().__proto__ === new USER() // true
new USER().__proto__ === USER.prototype // true
再進一步提煉以上兩個運算式,可獲得最終運算式,以下為實作繼承關鍵的完整原型鏈:
// VIP 建構式所生成的實體會經過兩層__proto__找到父類原型
new VIP().__proto__.__proto__ === USER.prototype
// 接下來,由 VIP 建構式生成的實體所沒有的屬性和方法,都會去父類原型找到屬性和方法,
原型鏈繼承缺點:
- 父類原型中若存在的參考值則會在所有實體間共享,
- 子類建構式在實體化時不能給父類建構式傳參,即我們的父類屬性無法重用,
為什么 VIP.prototype = new USER() 這一步要放到兩個建構式中間?
如果這一步運算式放到后面,我們的VIP.prototype是其原本建構式 VIP 的原型,在這個原本的建構式原型上添加方法,不會有繼承效果,
我們的想法是通過父類建構式生成實體,利用它實體的__proto__去實作繼承效果,要想在子類建構式添加方法,我們實際做了這樣的操作,如下:
// new USER()就是我們父類建構式生成的實體
new USER().addWeapon = function (weaponName) {
this.weapon.push(weaponName)
}
但是上面這樣會出現問題,我們子類建構式怎么辦?他想new一個實體,還是會根據原來的原型,
因此,我們需要將new USER()傳遞給VIP.prototype,這樣VIP建構式生成實體才會有繼承效果,如下:
VIP.prototype = new USER() // 傳遞__proto__實作繼承
VIP.prototype.addWeapon = function (weaponName) {
this.weapon.push(weaponName)
}
盜用建構式
來看這樣一段代碼:
function USER(username, password) {
this.username = username
this.password = password
this.weapon = ['水果小刀']
}
function VIP(username, password) {
USER.call(this, username, password) // 呼叫父類建構式,為其屬性賦值
} // 這里的 this 指向子類建構式生成的新實體
// 1. 接下來我們可以向父類建構式傳參
let a = new VIP('小紅')
console.log(a.username) // 小紅
// 2. 也可以解決參考值產生的問題
let b = new VIP()
b.weapon.push('屠龍寶刀')
console.log(b.weapon) // ['水果小刀', '屠龍寶刀']
let c = new VIP()
console.log(c.weapon) // ['水果小刀']
// 這樣實體 b 和 c 的武器串列的資料都是獨立的
程序決議:new VIP('小紅')傳入了一個“小紅”引數,
第一次系結操作:new執行程序會執行一次系結操作,將this指向實體物件,
第二次系結操作:VIP建構式內部的call方法再次系結實體物件,呼叫父類建構式
總結:我們通過傳參實際呼叫了兩次系結操作,最終使得子類建構式的新實體也能擁有父類的屬性和值,
盜用建構式缺點:
- 只能在建構式內部定義方法使用,不能訪問父類原型定義的方法,即我們的父類方法不能重用
組合繼承( = 原型鏈繼承 + 盜用建構式 )
如果你已經清楚的知道上面兩種繼承方式的優點和缺陷,那么我們可以利用1 + 1 > 2 的方法實作組合繼承,
function USER(username, password) {
this.username = username
this.password = password
this.weapon = ['水果小刀']
}
VIP.prototype = new USER()
// 注意:改寫原型,要記得把 constructor 指會原建構式
VIP.prototype.constructor = VIP
function VIP(username, password) {
USER.call(this, username, password) // 呼叫父類建構式,為其屬性賦值
} // 這里的 this 指向子類建構式生成的新實體
VIP.prototype.addWeapon = function (weaponName) {
this.weapon.push(weaponName)
}
// 我們嘗試給父類建構式傳參
let a = new VIP('小紅')
console.log(a.username) // 小紅
// 看看添加屠龍寶刀,有沒有相互影響
let b = new VIP()
b.addWeapon('屠龍寶刀')
console.log(b.weapon)
let c = new VIP()
console.log(c.weapon)
以上的組合繼承方式輸出了正確的答案,算是完美解決了原型鏈繼承和盜用建構式繼承出現的問題,我們將以上代碼放入瀏覽器打斷點分析,如下:
很明顯,我們第一次new USER()會呼叫父類建構式,而后子類建構式每一次生成新實體都會呼叫父類建構式,也就是說,多了第一次會呼叫父類建構式的情況,
原型繼承
在 JavaScirpt 高級程式設計 8.3.4 中提到了這種方式,來看這樣一段代碼
function object(obj) {
function Fn() { }
Fn.prototype = obj
return new Fn() // 回傳一個空函式,其內部原型改寫為 obj
}
有沒有熟悉的感覺,其實正是我們之前手寫bind函式利用的繼承方法,與ES6中的Object.create()方法效果相同,它適于在原有物件的基礎上再克隆一個物件,此外,物件屬性值若為原始值則可以進行改寫,若為參考值則會產生參考值的特點,即多個克隆物件會共享同一個參考值,也就是說這個“克隆”操作相當于我們的淺拷貝操作,
寄生繼承
function createAnother(obj) {
let clone = object(obj)
clone.sayHello = () => {
console.log('Hello World')
}
}
這種方式可以使克隆物件在原基礎上增強,即添加屬性和方法,
注意:原型繼承和寄生繼承都重點關注物件的使用,而不考慮建構式的使用
寄生組合繼承( = 組合繼承 + 原型繼承 + 寄生繼承 )
我們可以再利用瀏覽器打斷點試試,是不是不會發生像組合繼承那樣首次呼叫建構式的情況,
// 寄生組合繼承
function inheritPrototype(subType, superType) {
subType.prototype = Object.create(superType.prototype) // 創建物件
subType.prototype.constructor = subType // 增強物件
}
function USER(username, password) {
this.username = username
this.password = password
this.weapon = ['水果小刀']
}
// 驗證運算式時,下面這一條陳述句要加上注釋,
inheritPrototype(VIP, USER) // 呼叫繼承函式,
// 驗證運算式時,把下面這兩條陳述句注釋去掉,
// VIP.prototype = Object.create(USER.prototype) // 創建物件
// VIP.prototype.constructor = VIP // 增強物件
function VIP(username, password) {
USER.call(this, username, password) // 呼叫父類建構式,為其屬性賦值
} // 這里的 this 指向子類建構式生成的新實體
VIP.prototype.addWeapon = function (weaponName) {
this.weapon.push(weaponName)
}
// 我們嘗試給父類建構式傳參
let a = new VIP('小紅')
console.log(a.username) // 小紅
// 看看添加屠龍寶刀,有沒有相互影響
let b = new VIP()
b.addWeapon('屠龍寶刀')
console.log(b.weapon)
let c = new VIP()
console.log(c.weapon)
原型鏈__proto__實作繼承會經過的物件(從子類實體到父類原型):
- 子類建構式實體 :
new VIP() - 空建構式實體 :
Object.create(USER.prototype) - 父類建構式原型 :
USER.prototype
我們同樣構建兩個運算式去驗證:
new VIP().__proto__ === Object.create(USER.prototype) // true
Object.create(USER.prototype).__proto__ === USER.prototype // true
再進一步提煉以上兩個運算式,可獲得最終運算式,以下為實作繼承關鍵的完整原型鏈:
new VIP().__proto__.__proto__ === USER.prototype // true
// 接下來,由 VIP 建構式生成的實體所沒有的屬性和方法,都會去父類原型找到屬性和方法,
原型鏈和寄生組合的繼承區別比較
原型鏈的繼承實作:利用new USER()作為跳板實作繼承,
VIP.prototype = new USER() // 傳遞__proto__實作繼承
new VIP().__proto__ === new USER() // true
new USER().__proto__ === USER.prototype // true
寄生組合的繼承實作:利用Object.create(USER.prototype)作為跳板實作繼承,
VIP.prototype = Object.create(USER.prototype) // 傳遞__proto__實作繼承
new VIP().__proto__ === Object.create(USER.prototype) // true
Object.create(USER.prototype).__proto__ === USER.prototype // true
注意:Object.create(USER.prototype)會回傳一個空函式實體,這個實體的__proro__指向USER()建構式,
class繼承(ES6 語法)( ≈ 寄生組合繼承 )
在 ES5 之前我們都是利用建構式實作面向物件編程,ES6 的class作為語法糖,其實內部也是利用了建構式實作面向物件編程,
class USER {
constructor(username, password) {
this.username = username
this.password = password
this.weapon = ['水果小刀']
}
}
class VIP extends USER {
constructor(username, password) {
super(username, password) // 呼叫父類建構式,相當于執行 call 方法
}
addWeapon(weaponName) {
this.weapon.push(weaponName)
}
}
// 我們嘗試給父類建構式傳參
let a = new VIP('小紅')
console.log(a.username) // 小紅
// 看看添加屠龍寶刀,有沒有相互影響
let b = new VIP()
b.addWeapon('屠龍寶刀')
console.log(b.weapon)
let c = new VIP()
console.log(c.weapon)
// 以上同樣可運行我們的測驗代碼
總結 JavaScript 繼承方式:
- 原型鏈繼承
- 盜用建構式繼承
- 組合繼承( = 原型鏈繼承 + 盜用建構式 )
- 原型式繼承
- 寄生繼承
- 寄生組合繼承( = 組合繼承 + 原型繼承 + 寄生繼承 )
- class繼承( ≈ 寄生組合繼承 )
以上可以看出 JavaScript 對與繼承方式的優化是一個多次迭代不斷優化的程序,
參考
JavaScript高級程式設計(第4版)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/540553.html
標籤:其他
