JavaScript 原型概念深入理解
原型是JavaScript中一個比較難理解的概念,原型相關的屬性也比較多,物件有”[[prototype]]”屬性,函式物件有”prototype”屬性,原型物件有”constructor”屬性。
為了弄清原型,以及原型相關的這些屬性關係,就有了這篇文章。
相信通過這篇文章一定能夠清楚的認識到原型,現在就開始原型之旅吧。
認識原型
開始原型的介紹之前,首先來認識一下什麼是原型?
在JavaScript中,原型也是一個物件,通過原型可以實現物件的屬性繼承,JavaScript的物件中都包含了一個” [[Prototype]]”內部屬性,這個屬性所對應的就是該物件的原型。
“[[Prototype]]”作為物件的內部屬性,是不能被直接訪問的。所以為了方便檢視一個物件的原型,Firefox和Chrome中提供了”__proto__”這個非標準(不是所有瀏覽器都支援)的訪問器(ECMA引入了標準物件原型訪問器”Object.getPrototype(object)”)。
例項分析
下面通過一個例子來看看原型相關概念:
function Person(name, age){ this.name = name; this.age = age; this.getInfo = function(){ console.log(this.name + " is " + this.age + " years old"); }; } var will = new Person("Will", 28);
在上面的程式碼中,通過了Person這個建構函式建立了一個will物件。下面就通過will這個物件一步步展開了解原型。
Step 1: 檢視物件will的原型
通過下面程式碼,可以檢視物件will的原型:
console.log(will.__proto__); console.log(will.constructor);
結果分析:
- “Person {}”物件就是物件will的原型,通過Chrome展開可以看到,”Person {}”作為一個原型物件,也有”__proto__”屬性(對應原型的原型)。
-
在這段程式碼中,還用到了”constructor”屬性。在JavaScript的原型物件中,還包含一個”constructor”屬性,這個屬性對應建立所有指向該原型的例項的建構函式。
-
通過”constructor”這個屬性,我們可以來判斷一個物件是不是陣列型別
function isArray(myArray) { return myArray.constructor.toString().indexOf("Array") > -1; }
- 在這裡,will物件本身並沒有”constructor”這個屬性,但是通過原型鏈查詢,找到了will原型(will.__proto__)的”constructor”屬性,並得到了Person函式。
-
Step 2: 檢視物件will的原型(will.__proto__)的原型
既然will的原型”Person {}”也是一個物件,那麼我們就同樣可以來檢視”will的原型(will.__proto__)的原型”。
執行下面的程式碼:
console.log(will.__proto__ === Person.prototype); console.log(Person.prototype.__proto__); console.log(Person.prototype.constructor); console.log(Person.prototype.constructor === Person);
結果分析:
-
首先看 “will.__proto__ === Person.prototype”,在JavaScript中,每個函式都有一個prototype屬性,當一個函式被用作建構函式來建立例項時,該函式的prototype屬性值將被作為原型賦值給所有物件例項(也就是設定例項的__proto__屬性),也就是說,所有例項的原型引用的是函式的prototype屬性。瞭解了建構函式的prototype屬性之後,一定就明白為什麼第一句結果為true了。
- prototype屬性是函式物件特有的,如果不是函式物件,將不會有這樣一個屬性。
- 當通過”Person.prototype.__proto__”語句獲取will物件原型的原型時候,將得到”Object {}”物件,後面將會看到所有物件的原型都將追溯到”Object {}”物件。
- 對於原型物件”Person.prototype”的”constructor”,根據前面的介紹,將對應Person函式本身。
通過上面可以看到,“Person.prototype”物件和Person函式物件通過”constructor”和”prototype”屬性實現了相互引用(後面會有圖展示這個相互引用的關係)。
Step 3: 檢視物件Object的原型
通過前一部分可以看到,will的原型的原型是”Object {}”物件。實際上在JavaScript中,所有物件的原型都將追溯到”Object {}”物件。
下面通過一段程式碼看看”Object {}”物件:
console.log(Person.prototype.__proto__ === Object.prototype); console.log(typeof Object); console.log(Object); console.log(Object.prototype); console.log(Object.prototype.__proto__); console.log(Object.prototype.constructor);
通過下面的程式碼可以看到:
- Object物件本身是一個函式物件。
- 既然是Object函式,就肯定會有prototype屬性,所以可以看到”Object.prototype”的值就是”Object {}”這個原型物件。
- 反過來,當訪問”Object.prototype”物件的”constructor”這個屬性的時候,就得到了Obejct函式。
- 另外,當通過”Object.prototype.__proto__”獲取Object原型的原型的時候,將會得到”null”,也就是說”Object {}”原型物件就是原型鏈的終點了。
Step 4: 檢視物件Function的原型
在上面的例子中,Person是一個建構函式,在JavaScript中函式也是物件,所以,我們也可以通過”__proto__”屬性來查詢Person函式物件的原型。
console.log(Person.__proto__ === Function.prototype); console.log(Person.constructor === Function) console.log(typeof Function); console.log(Function); console.log(Function.prototype); console.log(Function.prototype.__proto__); console.log(Function.prototype.constructor);
結果分析 :
- 在JavaScript中有個Function物件(類似Object),這個物件本身是個函式;所有的函式(包括Function,Object)的原型(__proto__)都是”Function.prototype”。
- Function物件作為一個函式,就會有prototype屬性,該屬性將對應”function () {}”物件。
- Function物件作為一個物件,就有”__proto__”屬性,該屬性對應”Function.prototype”,也就是說,”Function.__proto__ === Function.prototype”
- 對於Function的原型物件”Function.prototype”,該原型物件的”__proto__”屬性將對應”Object {}”
對比prototype和__proto__
對於”prototype”和”__proto__”這兩個屬性有的時候可能會弄混,”Person.prototype”和”Person.__proto__”是完全不同的。
在這裡對”prototype”和”__proto__”進行簡單的介紹:
- 對於所有的物件,都有__proto__屬性,這個屬性對應該物件的原型
- 對於函式物件,除了__proto__屬性之外,還有prototype屬性,當一個函式被用作建構函式來建立例項時,該函式的prototype屬性值將被作為原型賦值給所有物件例項(也就是設定例項的__proto__屬性)
圖解例項
通過上面結合例項的分析,相信你一定了解了原型中的很多內容。
但是現在肯定對上面例子中的關係感覺很凌亂,一會兒原型,一會兒原型的原型,還有Function,Object,constructor,prototype等等關係。
現在就對上面的例子中分析得到的結果/關係進行圖解,相信這張圖可以讓你豁然開朗。
對於上圖的總結如下:
- 所有的物件都有”__proto__”屬性,該屬性對應該物件的原型
- 所有的函式物件都有”prototype”屬性,該屬性的值會被賦值給該函式建立的物件的”__proto__”屬性
- 所有的原型物件都有”constructor”屬性,該屬性對應建立所有指向該原型的例項的建構函式
- 函式物件和原型物件通過”prototype”和”constructor”屬性進行相互關聯
通過原型改進例子
在上面例子中,”getInfo”方法是建構函式Person的一個成員,當通過Person構造兩個例項的時候,每個例項都會包含一個”getInfo”方法。
var will = new Person("Will", 28); var wilber = new Person("Wilber", 27);
前面瞭解到,原型就是為了方便實現屬性的繼承,所以可以將”getInfo”方法當作Person原型(Person.__proto__)的一個屬性,這樣所有的例項都可以通過原型繼承的方式來使用”getInfo”這個方法了。
所以對例子進行如下修改:
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old"); };
修改後的結果為:
原型鏈
因為每個物件和原型都有原型,物件的原型指向物件的父,而父的原型又指向父的父,這種原型層層連線起來的就構成了原型鏈。
在”理解JavaScript的作用域鏈“一文中,已經介紹了識別符號和屬性通過作用域鏈和原型鏈的查詢。
這裡就繼續看一下基於原型鏈的屬性查詢。
屬性查詢
當查詢一個物件的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性為止,到查詢到達原型鏈的頂部(也就是 “Object.prototype”), 如果仍然沒有找到指定的屬性,就會返回 undefined。
看一個例子:
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.MaxNumber = 9999; Person.__proto__.MinNumber = -9999; var will = new Person("Will", 28); console.log(will.MaxNumber); // 9999 console.log(will.MinNumber); // undefined
在這個例子中分別給”Person.prototype “和” Person.__proto__”這兩個原型物件新增了”MaxNumber “和”MinNumber”屬性,這裡就需要弄清”prototype”和”__proto__”的區別了。
“Person.prototype “對應的就是Person構造出來所有例項的原型,也就是說”Person.prototype “屬於這些例項原型鏈的一部分,所以當這些例項進行屬性查詢時候,就會引用到”Person.prototype “中的屬性。
屬性隱藏
當通過原型鏈查詢一個屬性的時候,首先查詢的是物件本身的屬性,如果找不到才會繼續按照原型鏈進行查詢。
這樣一來,如果想要覆蓋原型鏈上的一些屬性,我們就可以直接在物件中引入這些屬性,達到屬性隱藏的效果。
看一個簡單的例子:
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old"); }; var will = new Person("Will", 28); will.getInfo = function(){ console.log("getInfo method from will instead of prototype"); }; will.getInfo(); // getInfo method from will instead of prototype
物件建立方式影響原型鏈
會到本文開始的例子,will物件通過Person建構函式建立,所以will的原型(will.__proto__)就是”Person.prototype”。
同樣,我們可以通過下面的方式建立一個物件:
var July = { name: "July", age: 28, getInfo: function(){ console.log(this.name + " is " + this.age + " years old"); }, } console.log(July.getInfo());
當使用這種方式建立一個物件的時候,原型鏈就變成下圖了,July物件的原型是”Object.prototype”也就是說物件的構建方式會影響原型鏈的形式。
hasOwnProperty
“hasOwnProperty”是”Object.prototype”的一個方法,該方法能判斷一個物件是否包含自定義屬性而不是原型鏈上的屬性,因為”hasOwnProperty” 是 JavaScript 中唯一一個處理屬性但是不查詢原型鏈的函式。
相信你還記得文章最開始的例子中,通過will我們可以訪問”constructor”這個屬性,並得到will的建構函式Person。這裡結合”hasOwnProperty”這個函式就可以看到,will物件並沒有”constructor”這個屬性。
從下面的輸出可以看到,”constructor”是will的原型(will.__proto__)的屬性,但是通過原型鏈的查詢,will物件可以發現並使用”constructor”屬性。
“hasOwnProperty”還有一個重要的使用場景,就是用來遍歷物件的屬性。
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old"); }; var will = new Person("Will", 28); for(var attr in will){ console.log(attr); } // name // age // getInfo for(var attr in will){ if(will.hasOwnProperty(attr)){ console.log(attr); } } // name // age
總結
本文介紹了JavaScript中原型相關的概念,對於原型可以歸納出下面一些點:
- 所有的物件都有”[[prototype]]”屬性(通過__proto__訪問),該屬性對應物件的原型
- 所有的函式物件都有”prototype”屬性,該屬性的值會被賦值給該函式建立的物件的”__proto__”屬性
- 所有的原型物件都有”constructor”屬性,該屬性對應建立所有指向該原型的例項的建構函式
- 函式物件和原型物件通過”prototype”和”constructor”屬性進行相互關聯
還有要強調的是文章開始的例子,以及通過例子得到的一張”普通物件”,”函式物件”和”原型物件”之間的關係圖,當你對原型的關係迷惑的時候,就想想這張圖(或者重畫一張當前物件的關係圖),就可以理清這裡面的複雜關係了。
通過這些介紹,相信一定可以對原型有個清晰的認識。
相關文章
- 深入理解JavaScript原型JavaScript原型
- 深入理解 JavaScript 原型JavaScript原型
- 深入理解javascript原型和閉包(3)——prototype原型JavaScript原型
- 深入理解javascript原型和閉包(4)——隱式原型JavaScript原型
- 深入理解JavaScript原型鏈與繼承JavaScript原型繼承
- 深入理解javascript原型和閉包系列JavaScript原型
- 深入理解javascript原型和閉包(10)——thisJavaScript原型
- 深入理解javascript原型和閉包(7)——原型的靈活性JavaScript原型
- 深入理解javascript原型和閉包(17)——補thisJavaScript原型
- 深入理解javascript原型和閉包(完結)JavaScript原型
- 理解JavaScript原型JavaScript原型
- 深入理解原型和原型鏈原型
- 深入理解javascript原型和閉包(16)——完結JavaScript原型
- 深入理解javascript原型和閉包(15)——閉包JavaScript原型
- 深入理解javascript原型和閉包(6)——繼承JavaScript原型繼承
- 深入理解javascript原型和閉包(5)——instanceofJavaScript原型
- 深入理解JS原型與原型鏈JS原型
- 深入理解原型物件和原型鏈原型物件
- 深入JavaScript系列(六):原型與原型鏈JavaScript原型
- JavaScript深入之從原型到原型鏈JavaScript原型
- JavaScript 深入之從原型到原型鏈JavaScript原型
- JS 中原型和原型鏈深入理解JS原型
- 如何理解JavaScript的原型和原型鏈?JavaScript原型
- 理解 JavaScript 中的原型JavaScript原型
- 深入理解javascript原型和閉包(12)——簡介【作用域】JavaScript原型
- 【譯】JavaScript 原型的深入指南JavaScript原型
- JavaScript之原型深入詳解JavaScript原型
- 三張圖理解JavaScript原型鏈JavaScript原型
- 理解JavaScript的原型屬性JavaScript原型
- 深入理解CRM的概念
- 深入理解javascript原型和閉包(11)——執行上下文棧JavaScript原型
- 深入理解原型鏈和繼承原型繼承
- 深入理解javascript原型和閉包(2)——函式和物件的關係JavaScript原型函式物件
- 深入理解javascript原型和閉包(1)——一切都是物件JavaScript原型物件
- 深入理解JavaScript物件JavaScript物件
- 由一張圖來理解javascript中的原型和原型鏈JavaScript原型
- 一張圖徹底理解Javascript原型鏈JavaScript原型
- 簡單粗暴地理解 JavaScript 原型鏈JavaScript原型