好好理解一下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
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...