首先明確一點,標準和實現是兩個概念。ECMAScript是標準,而不同瀏覽器廠商(vendor)可以根據此標準有不同的實現,這也是客戶端瀏覽器兼容性問題的原因之一。
__proto__與 prototype
在標準中,幾乎每個對象都有一個[[prototype]]屬性,且該屬性為隱藏屬性,并不對外暴露,指向這個對象的原型(即obj.prototype)。
但為了訪問需要,很多瀏覽器給對象提供了__proto__這一屬性,用以訪問上面提到的[[prototype]],因為瀏覽器實現并不一致,不建議直接使用__proto__。好在ES5(ECMAScript5.1版本)中增加了Object.getPrototypeOf()方法,用于獲得一個對象的[[prototype]],可用于替代__proto__。ES6中,則增加Object.setPrototypeOf(),可直接修改一個對象的[[prototype]]。
那問題來了,我們常說的prototype是什么呢?
//構造函數的prototype
console.log(Object.prototype); //Object {}
console.log(Array.prototype); //[Symbol(Symbol.unscopables): Object]
console.log(Function.prototype); //function () {}
console.log(Number.prototype); //Number {[[PrimitiveValue]]: 0}
console.log(RegExp.prototype); //Object {}
console.log(String.prototype); //String {length: 0, [[PrimitiveValue]]: ""}
console.log(Boolean.prototype); //Boolean {[[PrimitiveValue]]: false}
//實例的prototype
console.log(({name: 'yunmo'}).prototype); //undefined
console.log(([1,2]).prototype); //undefined
console.log((function(){}).prototype); //Object {}
console.log((1).prototype); //undefined
console.log(('yunmo').prototype); //undefined
console.log((true).prototype); //undefined
//實例的__proto__
console.log(({name: 'yunmo'}).__proto__); //Object {}
console.log(([1,2]).__proto__); //[Symbol(Symbol.unscopables): Object]
console.log((function(){}).__proto__); //function () {}
console.log((1).__proto__); //Number {[[PrimitiveValue]]: 0}
console.log(('yunmo').__proto__); //String {length: 0, [[PrimitiveValue]]: ""}
console.log((true).__proto__); //Boolean {[[PrimitiveValue]]: false}
如上代碼可以看出:
除了函數以外,實例對象是沒有prototype屬性的。只有函數才有prototype屬性。實例對象通過__proto__屬性([[prototype]])串起一個原型鏈,以此在原型鏈上查找屬性。即一個對象具有屬性__proto__,稱為隱式原型,該原型指向構造該對象的構造函數的原型,這保證了實例能夠訪問在構造函數原型中定義的屬性和方法。
每個函數都有一個屬性prototype。prototype的屬性值是一個對象,即屬性的集合,默認只有一個constructor屬性,指向這個函數本身。
如下圖:Super是一個構造函數,右側的方框就是它的原型。
function Yunmo() {
console.log('I AM YUNMO');
}
var yunmo = new Yunmo();
console.log(yunmo.constructor === Yunmo); //true
原型作為對象,不只擁有默認的constructor屬性,還允許自定義屬性。如Object構造函數的prototype上,擁有幾個我們熟悉的屬性。
一般來說有以下等式:
obj.__proto__ === obj.constructor.prototype
如果一個對象obj是通過Object.create函數構造出來的,那么obj.__proto__就不一定是obj.constructor.prototype。
var presetObject = {
name: 'yun',
age: 18
};
presetObject.prototype = Object.prototype;
var yunmo = Object.create(presetObject);
console.log(yunmo.__proto__ === yunmo.constructor.prototype); //false
constructor和prototype
看一下官方定義:
constructor
The value of a constructor's prototype property is a prototype object that is used to implement inheritance and shared properties.
prototype
object that provides shared properties for other objects
When a constructor creates an object, that object implicitly references the constructor's prototype property for the purpose of resolving property references.
Alternatively, a new object may be created with an explicitly specified prototype by using the Object.create built-in function.
譯:構造函數的原型的值是個原型對象,用來實現繼承和屬性共享。
原型是用來為其他對象提供共享屬性的對象。
當構造函數創建了一個對象,該對象將隱性地引用構造函數原型屬性,為了解決屬性引用問題。
或者,還可以通過內置的Object.create函數,指定一個明確的原型來創建一個新的對象。
簡單來說:prototype使得我們有能力向對象添加屬性和方法。
var Coder = function (name, age) {
this.name = name;
this.age = age;
this.learnCoding = function () {
console.log(this.name + ' is learning coding while he is ' + this.age);
}
};
var yun = new Coder('yun', 18);
var mo = new Coder('mo', 12);
yun.learnCoding();
mo.learnCoding();
console.log(yun.learnCoding === mo.learnCoding); //false
如上:一個構造函數Yun生成了yun和mo兩個實例對象,分別擁有name、age屬性和learnCoding方法。然而yun.learnCoding !== mo.learnCoding,說明每次通過new實例化一個對象的時候,都會去創建一個learnCoding方法。但是learnCoding方法做的其實是同樣的事情,卻不能被兩個實例所共享,就造成了內存資源的浪費。
為了避免上述問題,我們需要借助構造函數的原型對象來實現繼承和屬性共享。這時候,prototype就相當于一個公共容器,存放一些公用的東西。
var Coder = function (name, age) {
this.name = name;
this.age = age;
};
Coder.prototype.learnCoding = function () {
console.log(this.name + ' is learning coding while he is ' + this.age);
};
var yun = new Coder('yun', 18);
var mo = new Coder('mo', 12);
yun.learnCoding();
mo.learnCoding();
console.log(yun.learnCoding === mo.learnCoding); //true
原型鏈
對象在調用一個方法時會先在自身尋找是否有該方法,若沒有,則通過內部的__proto__屬性去自己的構造函數原型上尋找,如果還找不到,繼續向上追溯,直到Object.prototype.__proto__ = null為止,這種依靠原型查找形成的查詢鏈條,即原型鏈。
原型鏈的查找是根據什么進行的?prototype?__proto__?還是constructor?
理論上來說,誰都不是。因為prototype、__proto__ 和constructor這三個屬性是由js引擎對外暴露的API。如本文一開始所說的,對象內部存在一個隱藏屬性[[prototype]],外部是無法直接訪問的,js引擎通過提供__proto__屬性使得代碼上能夠訪問[[prototype]]。因此,從實際可操作的層面來說,是根據__proto__屬性進行查找的。