JavaScript 原型
JavaScript 常被描述為一種基于原型的語言 (prototype-based language)——每個對象擁有一個原型對象,對象以其原型為模板、從原型繼承方法和屬性。原型對象也可能擁有原型,并從中繼承方法和屬性,一層一層、以此類推。這種關系常被稱為原型鏈 (prototype chain),它解釋了為何一個對象會擁有定義在其他對象中的屬性和方法。(MDN)
每個對象都有一個標簽,這個標簽指向他的原型對象,對象基于一種機制,可以訪問到原型對象上的屬性。
在標準中,一個對象的原型是使用 [[prototype]]
表示的,chrome
對其的實現是使用 __proto__
屬性表示。
1. 什么是原型
1.1 屬性的訪問機制
在 JavaScript 中,除了幾種基礎類型,剩下的幾乎都是對象。
當我們使用對象自面量創(chuàng)建一個對象的時候,可以訪問到對象的 toString
方法。
var obj = { empty: true };
console.log(obj.toString()); // 輸出:[object Object]
在書寫這個自面量的時候,并沒有提供 toString
這個方法,卻可以被成功調用。
這就涉及到了原型。
當在訪問一個對象的屬性時,如果當前對象沒有這個屬性,就會繼續(xù)往這個對象的原型對象上去找這個屬性。
如果原型對象上沒有這個屬性,則繼續(xù)從這個 對象 的 原型對象 的 原型對象 找這個屬性。
這就是屬性查找的機制,直到查到原型的末端,也就是 null
,就會停止查找,這個時候已經確定沒有這個屬性了,就會返回 undefined
。
例子中的變量 obj
的原型可以通過 __proto__
訪問。
var obj = { empty: true };
console.log(obj.__proto__);
在輸出的原型對象中可以找到 toString
方法。
可以通過相等運算符來判斷調用的 toString
方法是不是原型上的方法。
var obj = { empty: true };
console.log(
obj.toString === obj.__proto__.toString,
); // 輸出:true
1.2 原型是怎么出現在一個對象上的
到這里有個問題,到底什么是原型,原型是怎么來的。
首先看一段代碼:
function Point(x, y) {
this.x = x;
this.y = y;
}
var point = new Point(1, 2);
console.log(point.__proto__);
這樣打印出來的 point
的原型對象,除了 constructor
和 __proto__
屬性,就什么都沒有了。
接下來做個改寫:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.info = function() {
console.log('x: ' + this.x + ', y: ' + this.y);
};
var point = new Point(1, 2);
point.info(); // 輸出:"x: 1, y: 2"
console.log(point.__proto__);
這樣輸出的 point
的原型對象,就具有了一個 info
方法。
從這就可以看出對象的原型,和他的構造函數的 prototype
屬性是有關的。
所有函數都具有一個 prototype
屬性,翻譯過來也是 原型的意思
。
當一個函數作為構造函數被調用的時候,就會把這個函數的 prototype
屬性,作為構造函數生成的對象的原型。
使用相等運算符,就可以驗證上面這個規(guī)則:
console.log(
point.__proto__ === Point.prototype,
); // 輸出:true
這就是一個對象原型的由來。
如果要知道對象由哪個構造函數生成,可以從 constructor
屬性獲取到,原型對象的 constructor
屬性則指向這個原型所處的函數。
這一點也可以由相等運算符驗證,point
對象的 constructor
屬性和其原型對象下的 constructor
應該都指向同一個,也就是 Point
函數。
console.log(
point.constructor === point.__proto__.constructor, // 輸出:true
point.constructor === Point, // 輸出:true
point.__proto__.constructor === Point, // 輸出:true
);
事實上對象的 constructor
屬性就是直接從原型上繼承的。
1.3 原型鏈
前面有提到訪問對象屬性的機制。
function Point(x, y) {
this.x = x;
this.y = y;
}
var point = new Point(1, 2);
console.log(point.toString());
假如要訪問 point
對象的 toString
方法,首先會去 point
類里找,很顯然是沒有這個方法的。
然后回去 point
類的原型對象上找,也就是 Point
函數的 prototype
屬性上,很顯然也是沒有的。
然后會再往上一層找,也就是找到了 Point.prototype.__proto__
上 (等同于 point.__proto__.__proto__
),這個時候就找到了 toString
,隨后被返回并且調用。
Point.prototype.__proto__
其實就是 Object.prototype
。
console.log(
Point.prototype.__proto__ === Object.prototype,
); // 輸出:true
假如檢查到 Object.prototype
還沒有目標屬性,則在往上就找不到了,因為 Object.prototype.__proto__
是 null
。
也就是說原型查找的末端是 null
,碰到 null
就會終止查找。
這些原型環(huán)環(huán)相扣,就形成了原型鏈
。
有些同學會有疑問,為什么 Point.prototype
的原型是 Object.prototype
。其實 Point.prototype
也是一個對象,可以理解成這個對象是通過 new Object
創(chuàng)建的,所以原型自然是 Object.prototype
。
2. proto 屬性
在 Chrome瀏覽器
下通過訪問對象的 __proto__
屬性可以取到對象的原型對象,這是所有對象都具備的屬性。
var date = new Date();
console.log(date.__proto__);
__proto__
具有兼容性問題,因此開發(fā)中盡量不要使用到,他不在 ES6
之前的標準中,但是許多舊版瀏覽器也對他進行了實現。
在 ES6
中 __proto__ 屬性
被定制成了規(guī)范。
3. Object.getPrototypeOf 方法
由于 __proto__
存在一定兼容性的問題,可以使用 Object.getPrototypeOf
方法代替 __ptoto__
屬性。
var date = new Date();
var dateProto = Object.getPrototypeOf(date);
console.log(dateProto);
console.log(dateProto === date.__proto__); // 輸出:true
4. JavaScript 中沒有類
在 JavaScript
中是沒有類的概念的。
有其他面向對象開發(fā)經驗的同學可能會被 new
關鍵字誤導。
JavaScript
中采用的是原型的機制,很多文獻會稱其為 原型代理
,但個人認為對于初學者使用 原型繼承
的方式會更好理解一點,日常討論中其實是一個意思,不需要過多糾正其說法。
類
和原型
是兩種不同的機制。
有關于類的內容,篇幅很大,如果不熟悉但又感興趣,可以嘗試著接觸一下其他面向對象的語言,如 Python
、Java
、C++
。
ES6 提供了
class
關鍵字,引入了一些類相關的概念,但其底層運行機制依然是原型這一套,所以即便是有了class
關鍵字來幫助開發(fā)者提升開發(fā)體驗,但其本質依然不是類,只是一種原型寫法的語法糖。
5. 小結
原型的概念至關重要,利用原型的機制可以開發(fā)出更加靈活的 JavaScript 應用。
利用原型,可以很好的復用一些代碼,雖然在 JavaScript
中沒有類,但是我們可以利用原型這個特性來模擬類的實現,達到繼承、多態(tài)、封裝的效果,實現代碼邏輯的復用,同時可以更好的組織代碼結構。