好好理解一下JavaScript中的原型


目錄

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]]進行關聯,對象關聯是原型中很重要的概念!

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM