一、原型
上回講到,生成一個對象我們可以通過new構造函數(shù)來實現(xiàn),如下:
function Person(name, age) {
this.name = name;
this.age = age;
this.getAge = function() {
return this.age;
}
}
var p = new Person('peter', 18);
但是,上面這樣也有個缺陷,比如每個person對象實際上都有getAge方法而且都一樣,但每次new的時候都要重新生成未免太浪費。那有沒有辦法把公共的方法找個地方存起來大家都去讀,然后每次只要生成自己私有的屬性和方法而不再生成共有的就可以了?
因為這點,原型就產生了,原型就是這個存公共方法的地方。一般我們把公共函數(shù)放在原型里,私有屬性和方法放在構造函數(shù)里,通過構造函數(shù)的prototype可以訪問到其對應的原型。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getAge = function() {
return this.age;
}
var p = new Person('peter', 18);
上面的代碼將公共的getAge挪到了Person的原型上,這樣每次new生成實例的時候跑Person函數(shù)里的代碼的時候就不會重復生成getAge方法了,而每個實例對象又能訪問到getAge。
其對應的原型圖如下,構造函數(shù)有個prototype指向其原型,每個原型又有個constructor指向其構造函數(shù),實例中的__proto__指向原型,這樣就能一一對應關系和互相訪問了。

二、原型鏈
上面的例子我們講的只是簡單畫了原型圖作為示例,實際上他的原型圖不止這三個。其實,所有的構造函數(shù)都有prototype指向原型,而所有的原型都有constructor指向其構造函數(shù),而每個實例對象都有一個__proto__指向生成它構造函數(shù)的原型。而原型其實也是個對象實例,通過new Object生成;構造函數(shù)也是個函數(shù)實例,通過new Function生成。所以上面的圖可以進一步完善成下面的樣子。

實際上,根據(jù)上面說的,原型其實也是個對象實例,通過new Object生成;構造函數(shù)也是個函數(shù)實例,通過new Function生成。那么,F(xiàn)unction.prototype也是個對象實例,而Object和Function也是函數(shù)實例,所以有
Function.prototype.__proto__ === Object.prototype
Object.__proto__ === Function.prototype
Function.__proto__ === Function.prototype
這其中要特別注意,Object.prototype實際上也是個對象實例,但js規(guī)定它的__proto__是指向null。所以有Object.prototype.__proto__ === null。于是上面的原型最后變成如下圖所示:

所以所謂的原型鏈,指的是每個實例對象都有自己的原型,而原型實際上也是個對象實例,他也有自己的原型,層層往上,從而形成一條原型鏈。所有的原型即prototype最終都會追溯到Object.prototype,而Object.prototype的原型是null,所以原型鏈的末端就是null。
// 從上圖的鏈,按著箭頭指向我們可以得到
p.__proto__.constructor.__proto__.__proto__.__proto__ === null // true
而當我們在一個實例里尋找某個屬性或方法時,js引擎會先在當前的實例上去找,如果找不到就往上一個原型里找,找到則返回,找不到再沿著原型鏈往上上個原型找,一直到末端的null,還沒有的話則返回undefined。
還是上面的例子:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getAge = function() {
return this.age;
}
var p = new Person('peter', 18);
p.getAge(); // 18
p.toString(); // [object Object]
- (1) 調用實例p.getAge方法,但是實例p上只有name和age屬性,沒有getAge;
(2) 于是往實例p的原型Person.prototype上去找,找到getAge方法返回,停止尋找。 - (1) 當調用p.toString方法時,實例p上只有name和age屬性找不到toString方法;
(2) 于是往p的原型Person.prototype上去找,但Person.prototype的原型只有getAge方法沒有toString;
(3) 于是繼續(xù)往Person.prototype的原型Obect.prototype里找,而Object.prototype默認定義了toString方法,所以返回該方法,停止尋找。
這就是為啥我們生成實例對象時,雖然沒有定義toString或valueOf方法,但仍能使用的原因,因為所有的原型鏈最終都能追溯到Object.prototype上,而Object.prototype默認定義了toString和valueOf方法。同理,其他原型也定義了一些公共方法給實例使用,比如生成數(shù)組實例時,雖然沒有定義pop、push等數(shù)組方法,卻能使用的原因,是因為在Array.prototype上已經(jīng)默認定義了這些方法。
三、原型相關方法
1. 獲得原型的三種方法
(1) 通過實例的__proto__來獲取,如obj.__proto__。但一般__proto__是瀏覽器為了實現(xiàn)原型鏈而增加的內在屬性,不是官方規(guī)定的方法不推薦。
(2) 通過實例的constructor.prototype來獲取,如obj.constructor.prototype。因為實例上找constructor屬性時會找到原型的constructor,可以獲得產生該實例的構造函數(shù),構造函數(shù)的prototype又會指向原型,所以間接獲得了原型。
(3) 通過Object.getPrototypeOf(obj)來獲取。此法是官方定義的方法,推薦用此法。
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person('peter', 18);
// 下面三種方法都可以獲得實例p的原型
p.__proto__;
p.constructor.prototype;
Object.getPrototypeOf(p);
2. 修改設置原型的三種方法
(1) 通過實例的__proto__來修改。該屬性可讀可寫,通過此法設置效果跟第三點一樣,不過不推薦用此法。
(2) 通過給構造函數(shù)的prototype直接賦值實現(xiàn)修改。注意用此法修改的時候要相應的修改prototype的constructor,否則會出現(xiàn)引用錯誤,詳見下面的例子。
(3) 通過Object.setPrototypeOf(obj, proObj)設置。官方定義的方法,推薦此法,不需要考慮constructor。
function Person(name) {
this.name = name;
}
Person.prototype.constructor === Person // true
// 法一
Person.prototype = {
//constructor: Person, // 應該加上這句,否則constructor不再指向對應的構造函數(shù)
method: function () {}
};
console.log(Person.prototype.constructor === Person) // false
console.log(Person.prototype.constructor === Object) // true
// 法二
Object.setPrototypeOf(Person, {
method: function () {}
});
console.log(Person.prototype.constructor === Person) // true
console.log(Person.prototype.constructor === Object) // false
3. 判斷原型
通過a.isPrototypeOf(b)可以判斷a是否是b的原型。
- 注意前面的getPrototypeOf()和setPrototypeOf()都是定義在Obejct上的,而此法是定義在Object.prototype上的,所以通過實例可以直接調用。
- 只要a在參數(shù)對象b的原型鏈上,該函數(shù)都會返回true。由于Object.prototype處于原型鏈的最頂端,所以對各種實例都返回true,只有直接繼承自null的對象除外。
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person('peter', 18);
Person.prototype.isPrototypeOf(p); // true
Object.prototype.isPrototypeOf(p); // true
Object.prototype.isPrototypeOf(Object.create(null)); // false,括號里面是以null為原型生成的實例