深入原型鏈與繼承(詳解JS繼承原理)

来源:https://www.cnblogs.com/chscript/archive/2022/12/22/16994957.html
-Advertisement-
Play Games

原型鏈與繼承 new 關鍵字的執行過程 讓我們回顧一下,this 指向里提到的new關鍵字執行過程。 創建一個新的空對象 將構造函數的原型賦給新創建對象(實例)的隱式原型 利用顯式綁定將構造函數的 this 綁定到新創建對象併為其添加屬性 返回這個對象 手寫new關鍵字的執行過程: function ...


目錄

原型鏈與繼承


new 關鍵字的執行過程

讓我們回顧一下,this 指向里提到的new關鍵字執行過程。

  1. 創建一個新的空對象
  2. 將構造函數的原型賦給新創建對象(實例)的隱式原型
  3. 利用顯式綁定將構造函數的 this 綁定到新創建對象併為其添加屬性
  4. 返回這個對象

手寫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__指向:實例對象

  1. 通過構造函數生成的實例對象
// 生成實例對象
function Foo() {}
let obj1 = new Foo()

// __proto__指向驗證
obj1.__proto__ === Foo.prototype // true
  1. 通過new Object()對象字面量生成的實例對象
// 生成實例對象
let obj2 = new Object()

// __proto__指向驗證
obj2.__proto__ === Object.prototype // true
  1. 通過functionclass聲明生成的實例對象
// 生成實例對象
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.prototypeFoo.prototype

Foo.prototype.__proto__ === Object.prototype // true

Function.prototype.__proto__ === Object.prototype // true

第三層__proto__指向:Object.prototype

Object.prototype.__proto__ === null // true

我們自己再畫一張圖看一下:

自底向上有三層的__proto__構成基本的JavaScript原型模式生態,最後再總結一下規則:

  1. 實例對象都會指向其構造函數原型
  2. 構造函數原型都會指向Object.prototype
  3. Object.prototype最終指向null

總結:其實我們的原型鏈指的就是__proto__的路徑。

註意:這裡只是為了原型鏈能更加直觀,請不要忘了構造函數原型的constructor屬性,它會指回對應的構造函數。


原型鏈繼承

我們利用任務驅動型的方法去學習繼承方式,考慮這樣一個類結構:

  1. 普通用戶:作為父類
  2. 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__實現繼承會經過的對象(從子類實例到父類原型):

  1. 子類構造函數實例 :new VIP()

  2. 父類構造函數實例new USER()

  3. 父類構造函數原型 :USER.prototype

我們可以構建兩個表達式去驗證:

new VIP().__proto__ === new USER() // true
new USER().__proto__ === USER.prototype // true

再進一步提煉以上兩個表達式,可獲得最終表達式。以下為實現繼承關鍵的完整原型鏈:

// VIP 構造函數所生成的實例會經過兩層__proto__找到父類原型
new VIP().__proto__.__proto__ === USER.prototype
// 接下來,由 VIP 構造函數生成的實例所沒有的屬性和方法,都會去父類原型找到屬性和方法。

原型鏈繼承缺點:

  1. 父類原型中若存在的引用值則會在所有實例間共用。
  2. 子類構造函數在實例化時不能給父類構造函數傳參,即我們的父類屬性無法重用。

為什麼 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 + 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__實現繼承會經過的對象(從子類實例到父類原型):

  1. 子類構造函數實例 :new VIP()
  2. 空構造函數實例Object.create(USER.prototype)
  3. 父類構造函數原型 :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 繼承方式:

  1. 原型鏈繼承
  2. 盜用構造函數繼承
  3. 組合繼承( = 原型鏈繼承 + 盜用構造函數 )
  4. 原型式繼承
  5. 寄生繼承
  6. 寄生組合繼承( = 組合繼承 + 原型繼承 + 寄生繼承 )
  7. class繼承( ≈ 寄生組合繼承 )

以上可以看出 JavaScript 對與繼承方式的優化是一個多次迭代不斷優化的過程。


參考

JavaScript高級程式設計(第4版)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 作者:KAiTO 文章來源:社區原創 往期回顧: 圖文結合帶你搞懂MySQL日誌之Redo Log(重做日誌) 圖文結合帶你搞懂InnoDB MVCC ...
  • 在我之前的一篇文章中,有引用一個討論用Hash還是Tree的問題,DB中關於查找類數據結構,除了樹,還有Hash(HashMap,HashSet)。 存儲數據結構之爭 B+樹主要是照顧磁碟IO這種特殊的性質應運而生的;然而在記憶體夠多夠大時,Hash某些時候比Tree結構有用得多。 但是Hash做索引 ...
  • 摘要:我們知道MyBatis和資料庫的交互有兩種方式有Java API和Mapper介面兩種,所以MyBatis的初始化必然也有兩種;那麼MyBatis是如何初始化的呢? 本文分享自華為雲社區《MyBatis詳解 - 初始化基本過程》,作者:龍哥手記 。 MyBatis初始化的方式及引入 MyBat ...
  • 手寫 Promise Promise 構造函數 我們先來寫 Promise 構造函數的屬性和值,以及處理new Promise()時會傳入的兩個回調函數。如下: class myPromise { constructor(func) { this.state = 'pending' // Promi ...
  • 本文簡介 點贊 + 關註 + 收藏 = 學會了 fabric.js 為我們提供了很多厲害的方法。今天要搞明白的一個東西是 canvas.interactive 。 官方文檔對 canvas.interactive 的解釋是: Indicates that canvas is interactive. ...
  • 案例介紹 歡迎來的我的小院,我是霍大俠,恭喜你今天又要進步一點點了!我們來用JavaScript編程實戰案例,做一個滑鼠愛心特效。滑鼠在頁面移動時會出現彩色愛心特效。通過實戰我們將學會createElement方法、appendChild方法、setTimeout方法。 案例演示 頁面出現後,滑鼠在 ...
  • HTML 介紹 引用 最全面的前端筆記來啦,包含了入門到入行的筆記,還支持實時效果預覽。小伙伴們不需要在花時間去寫筆記,或者是去網上找筆記了。面試高頻提問和你想要的筆記都幫你寫好了。支持移動端和PC端閱讀,深色和淺色模式。 原文鏈接:https://note.noxussj.top/ ::: war ...
  • 案例介紹 歡迎來到我的小院,我是霍大俠,恭喜你今天又要進步一點點了!我們來用JavaScript編程實戰案例,做一個大轉盤。當你難以抉擇的時候不妨用這個案例來幫你做選擇。通過編程實戰我們可以學到按鈕的點擊事件onclick()以及定時器的使用. 案例演示 每個選擇都展示在不同的盒子里,通過點擊中間的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...