目錄
Table of Contents generated with DocToc
- 目錄
- 一、參考書籍和資料
- 二、原型,[[prototype]]和.prototype以及constructor
- 三、原型鏈
- for...in和in運算子
- 四、屬性設定和屏蔽
- 五、JavaScript只有物件
- 六、建構式和new關鍵字
- 七、模仿類
- 八、對constructor的錯誤理解
- 九、原型繼承
- 十、類之間的關系
- 十一、總結
- 有問題就留言交流我很樂意
一、參考書籍和資料
翻看了幾本JS書籍,其中主要有以下幾本:《JavaScript高級程式設計第三版》、《你不知道的JavaScript卷一》、《JavaScript權威指南》以及查看了MDN檔案,文章主要說了JavaScript中原型的一些概念知識,花了一點時間去總結,如任何問題的話可以提出來一起交流解決,文章中的圖大多是從網路和書中截取下來,并非本人原創,
二、原型,[[prototype]]和.prototype以及constructor
結合書中的概念,原型是什么這個問題,可以這樣去解釋:原型就是一個參考(也就是指標),指向原型物件,這并不是廢話,很多人說原型,實際上沒意識到它只是一個參考,指向原型物件,原型在實體物件和建構式中有不同的名稱屬性,但總是指向原型物件,如圖所示:

三、原型鏈
要理解原型鏈,首先需要明白原型物件的作用就是讓所有實體物件共享它的屬性和方法,根據上圖,不難發現,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的原型鏈,
十一、總結
這里例舉幾點比較重要的概念:
- 進行物件屬性查找,首先會在當前物件查找,如果沒有就會繼續去查找內置[[prototype]]關聯的物件,這個原型鏈會一直到Object.prototype,如果還是找不到就回傳undefined,
- 建構式只是函式,沒有任何區別,使用new呼叫函式就是建構式呼叫,
- JavaScript沒有類,默認下不會復制,物件之間通過[[prototype]]進行關聯,物件關聯是原型中很重要的概念!
有問題就留言交流我很樂意
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/165956.html
標籤:JavaScript
上一篇:設計模式
下一篇:DOMContentLoaded vs jQuery.ready vs onload, How To Decide When Your Code Should Run
