深刻學習js系列是本身階段性成長的見證,但願經過文章的形式更加嚴謹、客觀地梳理js的相關知識,也但願可以幫助更多的前端開發的朋友解決問題,期待咱們的共同進步。前端
若是以爲本系列不錯,歡迎點贊、評論、轉發,您的支持就是我堅持的最大動力。git
在Brendan Eich大神爲JavaScript設計面向對象系統的時候,借鑑了Self
和Smalltalk
這兩門 基於原型的語言,之因此選擇基於原型的面向對象系統,並非由於時間匆忙,它設計起來相對簡單,而是由於從一開始Brendan Eich就沒打算在Javascipt中加入類的概念。github
以類爲中心的面向對象的編程語言中,類和對象的關係能夠想象成鑄模和鑄件的關係,對象老是從類中建立而來, 而在原型編程的思想中,類並非必須的,對象未必須要從一個類中建立而來。編程
JavaScript是一門徹底面向對象的語言,若是想要更好地使用JavaScript的面向對象系統,原型和原型鏈就是個繞不開的話題,設計模式
今天咱們就一塊兒來學習一下這方面的知識。數組
prototype
、__proto__
、constructor
見名知意,所謂的"鏈"描述的實際上是一種關係,加上原型兩個字,能夠理解爲原型之間的關係,既然是一種關係,就須要維繫,就比如咱們走親訪友,親情就是一種紐帶,類比在JavaScript當中——函數、對象實例、實例原型 也有自身的聯繫,而他們之間的紐帶就是下面這三個重要的屬性:瀏覽器
三個重要的屬性:prototype
、__proto__
、constructor
微信
咱們先來看看第一個屬性:prototype
閉包
所謂屬性,指的是一個事物的特徵,就好比美女的一大特徵是「大長腿」,那「大長腿"就是美女的屬性,類比到JavaScript中函數,每個函數都有一個prototype
屬性,這屬性就是與生俱來的特質。這裏須要特別強調一下,是函數,普通的對象是沒有這個屬性的,(這裏爲何說普通對象呢,由於在JavaScript裏面,一切皆爲對象,因此這裏的普通對象不包括函數對象)app
咱們來看一個例子:
function Person() {
}
// 雖然寫在註釋裏面,可是須要注意的是
// prototype 是函數纔會有的屬性 (哈哈哈,看來在JavaScript中函數果真是有特權的……)
Person.prototype.name = "Kevin";
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin
複製代碼
上面的代碼中咱們建立了一個構造函數Person
,而且在實例原型上面添加了一個name
屬性賦值爲"Kevin"
;
而後分別建立了兩個實例對象:person一、person2
;
當咱們打印兩個實例對象上name屬性時均輸出了Kevin
(能夠親自試一下)。
咱們不由疑惑,這個Person.prototype
究竟是什麼,爲何在上面添加屬性,在 構造函數的實例化對象上都能訪問到呢?
其實 Person這個函數的prototype
屬性指向了一個對象,即:Person.prototype
也是一個對象。(真是好多對象)這個對象正是調用該構造函數而建立的實例的原型。也就是這個例子中的person1
和person2
的原型。
爲了便於理解,咱們將上面的這段話拆解一下:
- 1.調用的構造函數: Person
- 2.使用什麼調用: new關鍵字
- 3.獲得了什麼: 兩個實例化對象person一、person2
- 4.實例化對象和原型是什麼關係: person1和person2的原型就是 Person.prototype
那什麼是原型呢?能夠這樣理解:每個JavaScript對象(null除外)在建立的時候就會與之關聯另一個對象,這個對象就是咱們所說的原型,而每個對象都會從原型"繼承"屬性。
上面的代碼中咱們並無直接在person1
和person2
中添加name屬性 可是這兩個對象 卻可以訪問name屬性,就是這個道理。
咱們用一張圖表示構造函數和實例原型之間的關係:
好了 構造函數和實例原型之間的關係咱們已經梳理清楚了,那咱們怎麼表示實例與實例原型,也就是person1
或者person2
和Person.prototype
之間的關係呢。這時候須要請出咱們理解原型鏈的第二個重要屬性__proto__
__proto__
這個屬性有什麼特徵呢?
其實這是每個JavaScript對象(除了null)都具備的一個屬性,叫__proto__,這個屬性會指向該對象的原型,即做爲實例對象和實例原型的之間的連接橋樑,這裏強調,是對象,一樣,由於函數也是對象,因此函數也有這個屬性。
咱們看一個代碼示例:
function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype); //true;
複製代碼
有了第二個屬性的幫助,咱們就能更加全面的理解這張關係圖了:
經過上面的關係圖咱們能夠看到,構造函數Person
和實例對象person
分別經過 prototype
和__proto__ 和實例原型Person.prototype
進行關聯,根據箭頭指向 咱們不由要有疑問:實例原型是否有屬性指向構造函數或者實例呢?
這時候該請出咱們的第三個屬性了:constructor
實例原型指向實例的屬性卻是沒有,由於一個構造函數可能會生成不少個實例,可是原型指向構造函數的屬性卻是有的,這就是咱們的constructor
——每個原型都有一個constructor
屬性指向關聯的構造函數。
咱們再來看一個示例:
function Person() {
}
console.log(Person === Person.prototype.constructor); // true
複製代碼
好了到這裏咱們再完善下關係圖:
經過對三個屬性的介紹,咱們總結一下:
function Person() {
}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 順便學習一個ES5的方法,能夠得到對象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
複製代碼
上述代碼中咱們咱們執行了如下操做:
- 1.聲明瞭構造函數 Person;
- 2.使用new操做符調用 Person 實例化了一個person 對象;
- 3.判斷實例化對象經過__proto__是否指向實例原型;
- 4.判斷實例原型經過constructor是否能找到對應的構造函數;
- 5.使用Object.getPrototypeOf方法傳入一個對象 找到對應的原型對象;
瞭解了構造函數。實例原型、和實例對象之間的關係,接下來咱們講講實例和原型的關係:
當讀取實例的屬性時,若是找不到,就會查找與對象關聯的原型中的屬性,若是還查不到,就去找原型的原型,一直找到最頂層爲止。
咱們再舉一個例子:
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
複製代碼
在上面這個例子中,咱們給實例person添加了name 屬性,當咱們打印person.name的時候,結果天然爲Daisy
可是當咱們刪除了person
下面的name屬性後,讀取person.name
,依然可以成功輸出Kevin,實際狀況是從 person 對象中找不到 name 屬性就會從 person 的原型也就是 person.__proto__
,也就是 Person.prototype
中查找,幸運的是咱們找到了 name 屬性,結果爲 Kevin。
可是咱們不由有疑問,若是萬一沒有找到該怎麼辦?
咱們來看下一層的關係 原型的原型
咱們前面提到過,原型也是一個對象,那麼既然是對象,那確定就有建立它的構造函數, 這個構造函數就是Object();
var obj = new Object();
obj.name = 'Kevin';
console.log(obj.name); // Kevin;
複製代碼
其實原型對象就是經過Object構造函數生成的,結合以前咱們所說的,實例__proto__指向構造函數的 prototype 因此咱們再豐富一下咱們的關係圖;
到了這裏咱們對於 構造函數、實例對象、實例原型之間的關係又有了進一步的認識。 說了這麼多,終於能夠介紹原型鏈了。
那Object.prototype 的原型呢?Object是根節點的對象,再往上查找就是null,咱們能夠打印:
console.log(Object.prototype.__proto__ === null) // true
複製代碼
然而 null 究竟表明了什麼呢?
引用阮一峯老師的 《undefined與null的區別》 就是:
null 表示「沒有對象」,即該處不該該有值。
因此 Object.prototype.proto 的值爲 null 跟 Object.prototype 沒有原型,其實表達了一個意思。
因此查找屬性的時候查到 Object.prototype 就能夠中止查找了。
咱們能夠將null 也加入最後的關係圖中,這樣就比較完整了。
上圖中相互關聯的原型組成的鏈狀結構就是原型鏈,也就是紅色的這條線
最後,補充三點你們可能不會注意到的地方:
首先是constructor,咱們看一個例子:
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
複製代碼
當獲取person.constructor
時,其實 person 中並無constructor
屬性,當不能讀取到constructor
屬性時,會從 person 的原型也就是 Person.prototype
中讀取,正好原型中有該屬性,因此:
person.constructor === Person.prototype.constructor
複製代碼
__proto__
其次是 proto ,絕大部分瀏覽器都支持這個非標準的方法訪問原型,然而它並不存在於 Person.prototype 中,實際上,它是來自於 Object.prototype ,與其說是一個屬性,不如說是一個 getter/setter,當使用 obj.proto 時,能夠理解成返回了 Object.getPrototypeOf(obj)。
最後是關於繼承,前面咱們講到「每個對象都會從原型‘繼承’屬性」,實際上,繼承是一個十分具備迷惑性的說法,引用《你不知道的JavaScript》中的話,就是:
繼承意味着複製操做,然而 JavaScript 默認並不會複製對象的屬性,相反,JavaScript 只是在兩個對象之間建立一個關聯,這樣,一個對象就能夠經過委託訪問另外一個對象的屬性和函數,因此與其叫繼承,委託的說法反而更準確些。
- 一、《Javascript設計模式與開發實踐》
- 二、JavaScript深刻之從原型到原型鏈
歡迎關注個人我的微信公衆號——指尖的宇宙,更多優質思考乾貨