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