深入學習js系列是自己階段性成長的見證,希望通過文章的形式更加嚴謹、客觀地梳理js的相關知識,也希望能夠幫助更多的前端開發的朋友解決問題,期待我們的共同進步。
如果覺得本系列不錯,歡迎點贊、評論、轉發,您的支援就是我堅持的最大動力。
開篇:
在Brendan Eich大神為JavaScript設計物件導向系統的時候,借鑑了Self
和Smalltalk
這兩門
基於原型的語言,之所以選擇基於原型的物件導向系統,並不是因為時間匆忙,它設計起來相對簡單,而是因為從一開始Brendan Eich就沒打算在Javascipt中加入類的概念。
以類為中心的物件導向的程式語言中,類和物件的關係可以想象成鑄模和鑄件的關係,物件總是從類中建立而來, 而在原型程式設計的思想中,類並不是必須的,物件未必需要從一個類中建立而來。
JavaScript是一門完全物件導向的語言,如果想要更好地使用JavaScript的物件導向系統,原型和原型鏈就是個繞不開的話題,
今天我們就一起來學習一下這方面的知識。
理解三個重要的屬性:prototype
、__proto__
、constructor
見名知意,所謂的"鏈"描述的其實是一種關係,加上原型兩個字,可以理解為原型之間的關係,既然是一種關係,就需要維繫,就好比我們走親訪友,親情就是一種紐帶,類比在JavaScript當中——函式、物件例項、例項原型 也有自身的聯絡,而他們之間的紐帶就是下面這三個重要的屬性:
三個重要的屬性:prototype
、__proto__
、constructor
prototype
我們先來看看第一個屬性:prototype
所謂屬性,指的是一個事物的特徵,就比如美女的一大特徵是“大長腿”,那“大長腿"就是美女的屬性,類比到JavaScript中函式,每一個函式都有一個prototype
屬性,這屬性就是與生俱來的特質。這裡需要特別強調一下,是函式,普通的物件是沒有這個屬性的,(這裡為什麼說普通物件呢,因為在JavaScript裡面,一切皆為物件,所以這裡的普通物件不包括函式物件)
我們來看一個例子:
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"
;
然後分別建立了兩個例項物件:person1、person2
;
當我們列印兩個例項物件上name屬性時均輸出了Kevin
(可以親自試一下)。
我們不禁疑惑,這個Person.prototype
到底是什麼,為什麼在上面新增屬性,在
建構函式的例項化物件上都能訪問到呢?
其實 Person這個函式的prototype
屬性指向了一個物件,即:Person.prototype
也是一個物件。(真是好多物件)這個物件正是呼叫該建構函式而建立的例項的原型。也就是這個例子中的person1
和person2
的原型。
為了便於理解,我們將上面的這段話拆解一下:
- 1.呼叫的建構函式: Person
- 2.使用什麼呼叫: new關鍵字
- 3.得到了什麼: 兩個例項化物件person1、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
——每一個原型都有一個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
首先是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 只是在兩個物件之間建立一個關聯,這樣,一個物件就可以通過委託訪問另一個物件的屬性和函式,所以與其叫繼承,委託的說法反而更準確些。
參考:
- 1、《Javascript設計模式與開發實踐》
- 2、JavaScript深入之從原型到原型鏈
深入學習JavaScript系列目錄
- #1 【深入學習js之——原型和原型鏈】
- #2 【深入學習js之——詞法作用域和動態作用域】
- #3 【深入學習js之——執行山下文棧】
- #4 【深入學習js之——變數物件】
- #5 【深入學習js之——作用域鏈】
- #6 【深入學習js之——實際開發場景中的this指向】
- #7 【深入學習js之——執行上下文】
- #8 【深入學習js之——閉包】
- #9 【深入學習js之——引數按值傳遞】
- #10 【深入學習js之——call和apply】
歡迎新增我的個人微信討論技術和個體成長。
歡迎關注我的個人微信公眾號——指尖的宇宙,更多優質思考乾貨