原來JS是這樣的 - 原型鏈

来源:https://www.cnblogs.com/blumia/archive/2019/11/17/Thats-JavaScript-prototype-chain.html
-Advertisement-
Play Games

上一篇提到屬性描述符 `[[Get]]` 和 `[[Put]]` 以及提到了訪問描述符 `[[Prototype]]`,看它們的特性就會很容易的讓人想到經典的面向對象風格體系中對類操作要做的事情,但帶一些 introspector 的味道。但我們前幾篇都沒有詳細的提及 js 的原型鏈相關的內容,本篇... ...


上一篇提到屬性描述符 [[Get]][[Put]] 以及提到了訪問描述符 [[Prototype]],看它們的特性就會很容易的讓人想到經典的面向對象風格體系中對類操作要做的事情,但帶一些 introspector 的味道。回想到之前所寫來自用的辣雞私有雲音樂應用中所附帶了一個簡易的類似 jQuery 的簡易常用功能實現,就用到了簡單的 [[Prototype]] 特性。但我們前幾篇都沒有詳細的提及 js 的原型鏈相關的內容,本篇就將討論 js 的 [[Prototype]] 屬性和相關的內容。

註:ES6 的 Proxy 和 class 的概念不在本篇討論範圍內。

[[Prototype]]

JavaScript 中的特殊對象屬性除了 [[Get]][[Put]] 外,還有一個很重要的特殊內置屬性就是 [[Prototype]] 了。

[[Prototype]] 是一個幾乎所有對象在創建時都會被賦予一個非空值的屬性,還記得在之前提到 new 操作符的行為嗎?其中的行為之一就是把其 [[Prototype]] 關聯指向到對應的內置對象上。通常 [[Prototype]] 所指向的即為創建此對象時所使用的對象了。

來看下麵一個例子

var macat = { a: 1 };

var codingcat = macat; // 和 macat 指向的內容相同
codingcat.b = 2;
console.log(macat.b); // 2

var pineapple = Object.create( macat ); // 新對象,但其 [[Prototype]] 鏈向 macat
pineapple.c = 3; // 新對象的屬性
console.log(macat.c); // undefined
codingcat.d = 4;
console.log(pineapple.d) // 4;

上例中, 變數 codingcat 顯然是指向和 macat 相同的內容,實質完全一致,而 pineapple 則是通過 Object.create() 創建的變數。顯然 pineapplemacat 是不同的兩個對象。不過我們會發現我們依然可以通過 pineapple.d 訪問 macat.d 的值,這就是因為在 Object.create() 中,會把 pineapple[[Prototype]] 指向我們的原型對象 macat 了。

[[Prototype]] 引用的作用是什麼呢?看上去這是一個確定這種像 fallback 一樣的取值操作應該 fallback 到誰的屬性標記,而準確的說,這種 pineapple.d 形式的屬性引用會觸發 [[Get]] 操作(上篇的內容),而預設的 [[Get]] 則會在對象本身沒有此屬性時會去查找 [[Prototype]] 引用的變數了。這樣的引用成為了鏈狀,故被稱作原型鏈。

當然,這個行為其實我們已經“用過”很多次了,比如 .toString().valueOf()hasOwnProperty(),我們 Object.create() 等形式構建的新對象顯然並沒有附帶一份這些函數的副本,而是因為普通的 [[Prototype]] 鏈最終都會指向內置的 Object.prototype,而它提供了這些功能。

屬性設置和屏蔽

不過上例中有個有趣的坑,我們考慮在上例的基礎上做如下操作:

...
pineapple.a++; // 互動式終端會輸出 1
console.log(pineapple.a); // 2
console.log(macat.a); // 1

pineapple.a++ 看上去是進行了變數自增的操作,但這一行後,我們發現 pineapple.a 不再等於 macat.a 了,這是因為實際上 pineapple.a 本來並不存在,但可以通過原型鏈找到 macat.a,而 pineapple.a++ (相當於 pineapple.a = pineapple.a + 1)最終進行的賦值操作創建了 pineapple.a ,故最終這兩個變數的值自然不再相等。

這個例子來看,如果本身即通過對 pineapple 的屬性(a)進行訪問操作,那麼不同情況下訪問得到的結果可能是不同的甚至是出人意料的。無意中創建的屬性“阻止”了原型鏈上查找這個屬性的行為,我們稱之為屬性屏蔽

屬性屏蔽根據變數本身情況的不同會有很多不同的狀態表現,例如原型鏈上層變數的數據訪問屬性標記為只讀的情況,(如果不是嚴格模式下)嘗試進行的賦值操作會被忽略等。

類 (迫真)

我們早已知道 JavaScript 中不存在“類”的概念,而為了能夠“寫著爽”,很多開發者都在想盡辦法在 JavaScript 中模仿其它 OO 語言中“類”的行為。其中很常見的做法類似下麵這樣:

function Person(name) {
    console.log("I'm " + name + "!");
    this.name = name;
}

Person.prototype.getName = function() {
    return this.name;
}

var chris = new Person("Chris"); // I'm Chris
var sophie = new Person("Sophie"); // I'm Sophie
chris.getName(); // "Chris"

看上去我們的 Person 像極了一個包含 name 成員變數和 getName() 方法的類,並且在其“構造函數”中會輸出 "I'm xxx"。不過在之前的文章中我們已經講過了,並不存在所謂的構造函數,new 只是把 Person() 函數作為構造對象所需調用的函數進行了一次調用而已。不過你可能還會比較奇怪為什麼 .getName() 是可以使用的,既然我們在原型鏈這一章提起這件事,顯然是因為原型鏈,於是回顧之前第二章我們含糊提到的一句話是(new 操作符所執行的操作步驟之一是)“對這個新對象執行 [[Prototype]] 鏈接”,實際上,這裡我們被 new 出來的對象的 [[Prototype]] 被關聯到了 Person.prototype 上,於是當我們嘗試進行屬性訪問的時候,自然就可以訪問到 Person.prototype.getName() 上了。

不過這個過程還是可能會引起一些蛋疼的誤會,比如假設我們在上面例子的基礎上:

...
sophie.constructor === Person; // true
sophie.constructor === Person.prototype.constructor; // true
Person.prototype = {};
var koishi = new Person("Koishi");  // I'm Koishi
koishi.constructor === Person; // false
koishi.constructor === Object; // true
sophie.constructor === Person; // true
sophie.constructor === Person.prototype.constructor; // false

由於“構造函數”這種表現形式的理解,我們有時候會認為 變數名.constructor 實際就總是構造調用時指向的函數,甚至 sophie.constructor === Person 返回也是 true ,但實際並不是這樣,這裡返回為真,僅僅是因為 Person.prototype.constructor 預設指向的就是 Person 罷了。於是我們嘗試替換 Person.prototype 之後創建了變數 koishi,再檢查 koishi.constructor === Person 就不再為真了,在原型鏈的查找過程最終找到了 Object.prototype,然後 Object.prototype.constructor 其實指向了 Object

不過,後面我們接著嘗試檢查了 sophie.constructor 卻發現似乎它並未受到影響,這個就不要往原型鏈方面想了,這裡的原因僅僅是 sophie 的原型鏈指向的是曾經 Person.prototype 所指向的東西上,而我們 Person.prototype = {} 的操作只是讓 Person.prototype 指向了新的東西,舊的東西並沒有改變,所以 sophie 自然看上去“沒有受到影響”了。當然,koishi 這個變數被構造時所被調用的函數仍然是 Person(),這和 koishi.constructor 或者 Person.prototype.constructor 的指向沒有什麼關係。

對象實例關係

當然我們還有一點需要重新強調的是,[[Prototype]].prototype 不是一回事,[[Prototype]] 是描述對象實例關係的屬性描述符,而 .prototype 只是 Function 對象的一個屬性而已。new 操作符會把新建的對象的 [[Prototype]] 指向原對象的 .prototype 屬性上,僅此而已。

既然 [[Prototype]] 實際描述了對象之間的實例關係,那麼我們自然就可以想到 instanceof 的實際作用了,其所做的事情就是告訴你在 a instanceof Foo 中, a 的整個原型鏈中是否有指向 Foo.prototype 的對象。

絕大多數瀏覽器支持一個 .__proto__ 屬性(實際位於 Object.__proto__)指向了 [[Prototype]] ,這對於我們調試時希望直接訪問內部的 [[Prototype]] 提供了便利,不過它並不是標準,所以除了調試便利之外還是不要使用它比較好。

最後

於是關於原型鏈相關的簡單討論就到此結束了。和上篇一樣,如果你對這些內容仍然感興趣,不妨去讀一讀《You don’t know JS - this & object prototypes》一書。這是一本開源書,你可以在這裡線上閱讀這本書,或者購買這本書的電子版或實體版。這本書的中文譯本涵蓋在《你所不知道的 JavaScript 上捲》中,你也可以考慮看中文版。

由於近期工作過於繁忙的精力占用緣故,“原來JS是這樣的”系列可能就暫時告一段落了。最後,儘管我會儘可能仔細的檢查文章內容是否有問題,但也不保證這篇文章中一定不會有錯誤,如果您發現文章哪裡有問題,請在下麵留言指正,或通過任何你找得到的方式聯繫我指正。感激不盡~


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

-Advertisement-
Play Games
更多相關文章
  • 前言 筆者所做的一個項目需要做一個前端的樹形菜單,後端返回的數據是一個平行的list,list中的每個元素都是一個對象,例如 的值為 ,每個元素都指定了父元素,生成的菜單可以無限級嵌套。一開始找的插件需要手動將生成好的樹形數組傳進去才能使用(儘管後來找到了一個UI框架,可以直接傳list進去,只需要 ...
  • 1. 定位 定位有三種:相對定位、絕對定位、固定定位 1.1 相對定位 現象和使用: 1.如果對當前元素僅僅設置了相對定位,那麼與標準流的盒子什麼區別。 2.設置相對定位之後,我們才可以使用四個方向的屬性: top、bottom、left、right。 特性:1.不脫標 2.形影分離 3.老家留坑 ...
  • 1. 盒模型 在CSS中,"box model"這一術語是用來設計和佈局時使用,然後在網頁中基本上都會顯示一些方方正正的盒子。我們稱為這種盒子叫盒模型。 盒模型有兩種:標準模型和IE模型。我們在這裡重點講標準模型。 1.1 盒模型示意圖 1.2 盒模型的屬性 width:內容的寬度 height: ...
  • [TOC] JavaScript簡介 JavaScript是前端的一門編程語言 node.js 支持前端js代碼可以跑在後端伺服器上 JavaScript 是腳本語言 JavaScript 是一種輕量級的編程語言。 JavaScript 是可插入 HTML 頁面的編程代碼。 JavaScript 插 ...
  • 1. CSS介紹 現在的互聯網前端分三層: HTML:超文本標記語言。從語義的角度描述頁面結構。 CSS:層疊樣式表。從審美的角度負責頁面樣式。 JS:JavaScript 。從交互的角度描述頁面行為 CSS:Cascading Style Sheet,層疊樣式表。CSS的作用就是給HTML頁面標簽 ...
  • 本文介紹了CSS常見的表格、浮動、定位佈局等方式,也介紹了聖杯佈局和雙飛翼佈局高級佈局的實現方式。 ...
  • 本文整理了CSS相關的基礎知識,包括CSS權重、雪碧圖、Base64編碼、自定義字體等知識點在CSS中的使用。 ...
  • ECMAScript 5 也稱為 ES5 和 ECMAScript 2009。 ECMAScript 5 特性 這些是 2009 年發佈的新特性: "use strict" 指令 String.trim() Array.isArray() Array.forEach() Array.map() Ar ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...