好好理解一下JavaScript中的原型

来源:https://www.cnblogs.com/wljqds/archive/2019/12/04/js_prototype.html
-Advertisement-
Play Games

目錄 Table of Contents generated with "DocToc" "目錄" "一、參考書籍和數據" "二、原型,[[prototype]]和.prototype以及constructor" "三、原型鏈" "for...in和in操作符" "四、屬性設置和屏蔽" "五、Jav ...


目錄

Table of Contents generated with DocToc

一、參考書籍和數據

翻看了幾本JS書籍,其中主要有以下幾本:《JavaScript高級程式設計第三版》、《你不知道的JavaScript捲一》、《JavaScript權威指南》以及查看了MDN文檔。文章主要說了JavaScript中原型的一些概念知識,花了一點時間去總結,如任何問題的話可以提出來一起交流解決。文章中的圖大多是從網路和書中截取下來,並非本人原創。

二、原型,[[prototype]]和.prototype以及constructor

結合書中的概念,原型是什麼這個問題,可以這樣去解釋:原型就是一個引用(也就是指針),指向原型對象。這並不是廢話,很多人說原型,實際上沒意識到它只是一個引用,指向原型對象。原型在實例對象和構造函數中有不同的名稱屬性,但總是指向原型對象。如圖所示:

[[prototype]]和.prototype以及constructor

  • 其中的constructor是原型對象的屬性,引用的是對象關聯的函數,不可枚舉,但這個屬性是可以修改的,因此不可靠。
  • 在實例對象中,原型就是對象的[[prototype]]內置屬性(雙方括弧代表這是JavaScript引擎內部使用的屬性/方法,正常JS代碼無法訪問,但可以通過__proto__訪問到,後面會說到),在對象被創建時就包含了該屬性,指向它的構造函數的原型對象。
  • 在函數中,原型就是函數的.prototype屬性,在函數被創建時就包含該屬性,指向構造函數的原型對象 。

三、原型鏈

要理解原型鏈,首先需要明白原型對象的作用就是讓所有實例對象共用它的屬性和方法。根據上圖,不難發現,person1和person2中的內部屬性[[prototype]]都指向Person原型對象。當進行對象屬性查找的時候,比如person1.name,首先會檢查對象本身是否有這個屬性,如果沒有就繼續去查找該對象[[prototype]]指向的原型對象中是否有該屬性,如果還是沒有就繼續去找這個原型對象的[[prototype]]指向的原型對象(註意,原型對象也是有他自己的[[prototype]]屬性的)!這個過程會持續找到匹配的屬性名或查找完整的原型鏈。不難理解了,原型鏈就是:每個實例對象( object )都有一個私有屬性(稱之為[[prototype]])指向它的構造函數的原型對象(prototype )。該原型對象也有一個自己的原型對象( [[prototype]] ) ,層層向上直到一個對象的原型對象為Object.prototype(因為所有對象都是源於Object.prototype,其中包含許多通用的功能方法)。顯然,如果找完這個原型鏈都找不到就會返回undefined。這個過程可以用一張圖描述:

顯然,原型和原型鏈的作用就是:如果對象上沒有知道需要的屬性和方法引用,JS引擎就會繼續在[[prototype]]關聯的對象上進行查找。這也是原型和原型鏈存在的意義。

for...in和in操作符

兩個跟原型鏈有關的操作

  • for...in遍歷對象時,任何可以通過原型鏈訪問到的(並且是enumerable為true)屬性都會被枚舉。
  • in操作符用於檢測屬性在對象中是否存在,同樣是會查找整條原型鏈。
function Person(name){
  this.name = name;
}
Person.prototype.sayName = function() {
  return this.name;
}
let myObject = new Person('練習生');
// 輸出兩個屬性:name和sayName,其中sayName是原型對象中的屬性
for(let key in myObject) {
  console.log(key);
}
// 輸出true,表示不可枚舉的constructor存在於myObject中。
// 事實上constructor是在Person.prototype對象中
console.log("constructor" in myObject);

四、屬性設置和屏蔽

給對象設置屬性並不僅僅是添加一個屬性或修改已有屬性。這個過程應該是這樣的:

// myObject的聲明在第一個代碼塊

// 註意:sayName在Person.prototype中存在,將屏蔽原型鏈上的sayName方法
myObject.sayName = function() {
  return `my name is:${this.name}`;
}
// 註意:age在myObject的整個原型鏈都不存在,將在實例中新建age屬性
myObject.age = 23;

// 完成上述對myObject屬性的設置,再新建一個對象
let myObject_1 = new Person('James');

// 查找myObject的屬性和方法
myObject.age; //23
myObject.sayName(); // my name is: Bob

// 查找myObject_1的屬性和方法
myObject.age; // undefined
myObject.sayName(); // 'Cat'

直接設置實例屬性,都會屏蔽原型鏈上的所有同名屬性(前提是屬性的writable為 true,並且屬性沒有setter),並有以下兩種情況:

  • 當sayName屬性不直接存在對象中而存在於原型鏈上層時,將會在myObjet中直接添加sayName屬性,註意它只會阻止訪問原型鏈上層的sayName屬性,但不會修改按個屬性。
  • 當原型鏈上找不到age,則age直接添加到myObject中。

五、JavaScript只有對象

在面向對象語言中,類是可以被實例化多次,就像使用模具製作東西一樣,對於每一個實例都會重覆這個過程。但在JavaScript中,沒有類,沒有複製機制。只能創建多個對象,通過它們的內置[[prototype]]關聯同一個原型對象。預設情況下,它們是關聯的,並非複製,因為是同一個原型對象所以它們之間也不會完全失去聯繫。

比如說,new Person()生成一個對象,同時這個新對象的內置[[prototype]]關聯的是Person.prototype對象。這裡得到了兩個對象,它們之間僅僅互相關聯,並沒有初始化類,如圖所示:

這種機制也就是所謂的原型繼承。這種Person()函數不算是類,它只是利用了函數的prototype屬性“模仿類”而已!所以說,JavaScript沒有類只有對象。

六、構造函數和new關鍵字

文章第一個代碼塊很容易讓人認為Person是一個構造函數,因為使用new調用並看到他構造了一個對象。但其實Person跟其他普通函數沒有什麼不同,函數本身不是構造函數,所有的一切只是在函數調用前加了new關鍵字!這樣就會把這個函數調用變成一個“構造函數調用”。new會劫持所有普通函數並用構造對象的形式去調用它。下麵這段代碼可以證明這點:

function BaseFunction() {
  console.log('Not a constructor!');
}
let myObject = new BaseFunction();
// Not a constructor.
typeof myObject; // object

BaseFunction是一個普通函數並非構造函數,但通過new調用,卻會構造出一個對象。因此,構造函數其實是所有帶new的函數調用。

七、模仿類

前面已經明確說過,JavaScript中只有對象,沒有真正的類,但JavaScript開發者通過下麵兩種方法可以模擬類,如下代碼所示:

function Foo(name) {
  this.name = name;
}
Foo.prototype.myName = function() {
  return this.name;
}
let a = new Foo('a');
let b = new Foo('b');

a.myName(); // a
b.myName(); // b
  • this.name = name 給每一個new調用構造出來的對象都添加了.name屬性(this綁定當前對象),這有點類似面向對象中“類實例封裝的數據值”。
  • Foo.prototype.myName = ...,給原型對象添加方法,那麼通過該構造函數調用創建的實例就能共用原型對象的方法和屬性。因此,a.myName和b.myName都可以正常工作,這有點類似面向對象中的什麼?這點我還不知道,反正就是面向對象設計模式的一種。有知道的可以留言告訴我。

八、對constructor的錯誤理解

接上面的代碼所示,如果繼續運行a.constructor === Foo,返回的是true,因此有這種錯誤觀點:對象由Foo構造。現在是時候把這個錯誤觀點改過來了。constructor是存在於Foo.prototype中,a對象只是[[prototype]]委托找到constructor!這和構造毫無關係,下麵代碼可以證明這一點:

function Foo(){}
//將Foo的原型對象指向一個空對象
Foo.prototype = {};
let a = new Foo();
a.constructor === Foo; //false
a.constructor === Object; // true

嗯哼?現在你還敢說constructor表示a由Foo構建嗎?按照這種錯誤觀點,a.constructor === Foo應該返回true!其實constructor在只是創建函數時一個預設屬性,指向prototype屬性所在的函數。constructor屬性時可以被修改的,讓原型對象指向新的對象的時候,為了讓constructor指向之前的函數,可以手動使用defineProperty方法添加一個不可枚舉constructor屬性。但真的很麻煩,總而言之不要太信任constructor屬性!

九、原型繼承


從這張圖,可看出三點

  • a1/a2到Foo.prototype,b1/b2到Bar.prototype的委托關聯
  • Bar.Prototype到Foo.prototype的委托關聯
  • 箭頭由下到上表明這是委托關聯而不是複製操作,否則如果是複製操作箭頭應該回事由上往下。
    下麵這段代碼是典型的原型繼承風格
function Foo(name){
  this.name = name;
}
Foo.prototype.myName = function() {
  return this.name;
}
function Bar(name, label) {
  Foo.call(this, name);
  this.label = label;
}
// 將新的Bar原型對象和Foo的原型對象進行關聯
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.myLabel = function() {
  return this.label;
}
let a = new Bar("a", "obj a");
a.myName();
a.myLabel();
  • 上面代碼中,Bar.prototype = Object.create(Foo.prototype)表示創建新的Bar.prototype對象並關聯到Foo.Prototype中。註意,這其實是把舊的Bar.prototype對象拋棄掉,再引用新的已關聯到Foo.prototype的對象。
  • ES6新增Object.setPrototypeOf(obj1, obj2),表示直接將obj1的[[prototype]]關聯到為obj2。

以下兩行代碼都是錯誤的對象關聯做法:

Bar.prototype = Foo.prototype;

Bar.prototype = new Foo();
  • 第一行代碼只是讓Bar的原型對象直接引用Foo的原型對象。如果對Bar.prototype的屬性進行修改,則會影響到Foo.prototype本身。
  • 第二行代碼,在《JavaScript高級程式設計第三版》的示例代碼出現。一開始覺得沒問題,後來在《你不知道的JavaScript》中,它指出是錯誤的做法,原因是Foo函數如果會有一些副作用(比如給this添加數據就很不好),會影響到Bar()的實例。

十、類之間的關係

檢查一個實例和祖先通常稱為反射或內省。在JavaScript中通常用到

  • 使用a instanceof Foo操作符,instanceof表示的是:在對象a的原型鏈上是否有指向Foo.prototype的對象。註意,instanceof的左側是對象,右側是函數。
  • 使用a.isPrototypeOf(b),isPrototypeOf表示的是:在對象a的整條原型鏈上是否出現過b。
  • 使用Object.getPrototypeOf(a),可以直接得到一個對象a的原型鏈。

十一、總結

這裡例舉幾點比較重要的概念:

  1. 進行對象屬性查找,首先會在當前對象查找,如果沒有就會繼續去查找內置[[prototype]]關聯的對象,這個原型鏈會一直到Object.prototype,如果還是找不到就返回undefined。
  2. 構造函數只是函數,沒有任何區別,使用new調用函數就是構造函數調用。
  3. JavaScript沒有類,預設下不會複製,對象之間通過[[prototype]]進行關聯,對象關聯是原型中很重要的概念!

有問題就留言交流我很樂意


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

-Advertisement-
Play Games
更多相關文章
  • 轉載請標明出處:https://www.cnblogs.com/tangZH/p/11985745.html 有些手機中,給TextView設置lineSpacingExtra後會出現最後一行的文字也出現lineSpacingExtra,不是某些版本才會,這跟機型有關。 可以用下麵這種方法解決: ...
  • 作為JavaScript開發人員,NPM是我們一直使用的東西,並且我們的腳本在終端上連續運行。 如果我們可以節省一些時間呢? 1、直接從npm打開文檔 如果我們可以直接使用npm跳轉到軟體包的文檔怎麼辦? 2、打開bug頁面 為了以防萬一,我們想在程式包上提交一個錯誤。 如果有這個包的作者的鏈接,將 ...
  • 在web開發時,可能經常會用到sessionstorage存儲數據,存儲單個字元串數據變數時並不困難 var str = 'This is a string'; sessionstorage.setItem('param',str); 獲取sessionstorage var item = sess ...
  • js Brendan(布蘭登) Eich 輕量級的編程語言(ECMAscript5或6), 是一種解釋性腳本語言(代碼不進行預編譯), 主要用來向HTML頁面添加交互行為, 目前是互聯網上最流行的腳本語言, 支持面向對象、命令式和聲明式(如函數式編程)風格, JavaScript,他和Python一 ...
  • 事故起源於一個魔鬼測試人員,某天做網站UI優化的時候,突然甩了一個問題給我 第二列的數據是可以跳轉至其他頁面的,但是,魔鬼測試的電腦上,一直都有一條數據是與其他的樣式不同,於是便甩了這個問題給我,我一瞅,喲呵,還真是,而且不管如何F5,Ctrl + F5,都不能改變它變黑的事實,一時間都不敢回消息了 ...
  • 從輸入URL到頁面載入發生了什麼? 最近在進行前端性能優化方面的一些工作,發現前端性能方面太廣,不知道如何下手。參考了許多文章,發現最終都會歸咎於一個非常經典的問題: 從輸入URL到頁面載入發生了什麼? 通過連接這個過程,然後針對性地對每個過程進行優化,最終實現的就是我們的前端性能優化。本篇文章主要 ...
  • 一、解決什麼問題 1、開發環境js、css不壓縮,可在瀏覽器選中代碼調試 2、開發環境運行http服務指向打包後的文件夾 3、babel輸出瀏覽器相容的js代碼 二、需要安裝的包 babel-loader:輸出瀏覽器相容的js代碼;命令:<!--?xml version="1.0" encoding ...
  • At a Glance Script tags have access to any element which appears before them in the HTML. jQuery.ready / DOMContentLoaded occurs when all of the HTML ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...