白話原型和原型鏈

艾特老幹部發表於2017-08-23

關於原型和原型鏈的介紹,網上數不勝數,但能講清楚這兩個概念的很少,大多數都是介紹各種物件、屬性之間如何指來指去,最後的結果就是箭頭滿天飛,大腦一團糟。本文將從這兩個概念的命名入手,用通俗易懂的語言,幫助你理解這兩個東西到底是何方神聖。

一. 背景知識

JavaScript和Java、C++等傳統物件導向的程式語言不同,它是沒有類(class)的概念的(ES6 中的class也只不過是語法糖,並非真正意義上的類),而在JavaScript中,一切皆是物件(object)。在基於類的傳統物件導向的程式語言中,物件由類例項化而來,例項化的過程中,類的屬性和方法會拷貝到這個物件中;物件的繼承實際上是類的繼承,在定義子類繼承於父類時,子類會將父類的屬性和方法拷貝到自身當中。因此,這類語言中,物件建立和繼承行為都是通過拷貝完成的。但在JavaScript中,物件的建立、物件的繼承(更好的叫法是物件的代理,因為它並不是傳統意義上的繼承)是不存在拷貝行為的。現在讓我們忘掉類、忘掉繼承,這一切都不屬於JavaScript。

二. 原型和原型鏈

其實,原型這個名字本身就很容易產生誤解,原型在百度詞條中的釋義是:指原來的型別或模型。按照這個定義解釋的話,物件的原型是物件建立自身的模子,模子具備的特點物件都要具有,這儼然就是拷貝的概念。我們已經說過, JavaScript的物件建立不存在拷貝,物件的原型實際上也是一個物件,它和物件本身是完全獨立的兩個物件。既然如此,原型存在的意義又是什麼呢?原型是為了共享多個物件之間的一些共有特性(屬性或方法),這個功能也是任何一門物件導向的程式語言必須具備的。A、B兩個物件的原型相同,那麼它們必然有一些相同的特徵。

JavaScript中的物件,都有一個內建屬性[[Prototype]],指向這個物件的原型物件。當查詢一個屬性或方法時,如果在當前物件中找不到定義,會繼續在當前物件的原型物件中查詢;如果原型物件中依然沒有找到,會繼續在原型物件的原型中查詢(原型也是物件,也有它自己的原型);如此繼續,直到找到為止,或者查詢到最頂層的原型物件中也沒有找到,就結束查詢,返回undefined。可以看出,這個查詢過程是一個鏈式的查詢,每個物件都有一個到它自身原型物件的連結,這些連結元件的整個鏈條就是原型鏈。擁有相同原型的多個物件,他們的共同特徵正是通過這種查詢模式體現出來的。

在上面的查詢過程,我們提到了最頂層的原型物件,這個物件就是Object.prototype,這個物件中儲存了最常用的方法,如toStringvalueOfhasOwnProperty等,因此我們才能在任何物件中使用這些方法。

1.字面量方式

當通過字面量方式建立物件時,它的原型就是Object.prototype。雖然我們無法直接訪問內建屬性[[Prototype]],但我們可以通過Object.getPrototypeOf()或物件的__proto__獲取物件的原型。

var obj = {};
Object.getPrototypeOf(obj) === Object.prototype;   // true
obj.__proto__  === Object.prototype;            // true
複製程式碼

2.函式的構造呼叫

通過函式的構造呼叫(注意,我們不把它叫做建構函式,因為JavaScript中同樣沒有建構函式的概念,所有的函式都是平等的,只不過用來建立物件時,函式的呼叫方式不同而已)也是一種常用的建立物件的方式。基於同一個函式建立出來的物件,理應可以共享一些相同的屬性或方法,但這些屬性或方法如果放在Object.prototype裡,那麼所有的物件都可以使用它們了,作用域太大,顯然不合適。於是,JavaScript在定義一個函式時,同時為這個函式定義了一個 預設的prototype屬性,所有共享的屬性或方法,都放到這個屬性所指向的物件中。由此看出,通過一個函式的構造呼叫建立的物件,它的原型就是這個函式的prototype指向的物件。

var f = function(name) { this.name = name };
f.prototype.getName = function() { return this.name; }   //在prototype下存放所有物件的共享方法
var obj = new f('JavaScript');
obj.getName();                  // JavaScript
obj.__proto__ === f.prototype;  // true
複製程式碼

3.Object.create()

第三種常用的建立物件的方式是使用Object.create()。這個方法會以你傳入的物件作為建立出來的物件的原型。

var obj = {};
var obj2 = Object.create(obj);
obj2.__proto__ === obj;       // true
複製程式碼

這種方式還可以模擬物件的“繼承”行為。

function Foo(name) {
	this.name = name;
}

Foo.prototype.myName = function() {
	return this.name;
};

function Bar(name,label) {
	Foo.call( this, name );   //
	this.label = label;
}

// temp物件的原型是Foo.prototype
var temp = Object.create( Foo.prototype );  

// 通過new Bar() 建立的物件,其原型是temp, 而temp的原型是Foo.prototype,
// 從而兩個原型物件Bar.prototype和Foo.prototype 有了"繼承"關係
Bar.prototype = temp;

Bar.prototype.myLabel = function() {
	return this.label;
};

var a = new Bar( "a", "obj a" );

a.myName(); // "a"
a.myLabel(); // "obj a"
a.__proto__.__proto__ === Foo.prototype;  //true
複製程式碼

三. __proto__和prototype

這是容易混淆的兩個屬性。__proto__指向當前物件的原型,prototype是函式才具有的屬性,預設情況下,new 一個函式建立出的物件,其原型都指向這個函式的prototype屬性。

四. 三種特殊情況

1.對於JavaScript中的內建物件,如String、Number、Array、Object、Function等,因為他們是native程式碼實現的,他們的原型列印出來都是ƒ () { [native code] }

2.內建物件本質上也是函式,所以可以通過他們建立物件,建立出的物件的原型指向對應內建物件的prototype屬性,最頂層的原型物件依然指向Object.prototype。

'abc'.__proto__ === String.prototype;   // true 
new String('abc').__proto__ === String.prototype;  //true

new Number(1).__proto__  ==== Number.prototype;   // true

[1,2,3].__proto__ === Array.prototype;            // true
new Array(1,2,3).__proto__ === Array.prototype;   // true

({}).__proto__ === Object.prototype;               // true 
new Object({}).__proto__ === Object.prototype;     // true

var f = function() {};
f.__proto__ === Function.prototype;            // true
var f = new Function('{}');
f.__proto__ === Function.prototype;            // true
複製程式碼

3.Object.create(null) 建立出的物件,不存在原型。

var a = Object.create(null); 
a.__proto__;               // undefined
複製程式碼

此外,函式的prototype中還有一個constructor方法,建議大家就當它不存在,它的存在讓JavaScript原型的概念變得更加混亂,而且這個方法也幾乎沒有作用。


歡迎關注我的公眾號:老幹部的大前端,領取21本大前端精選書籍!

白話原型和原型鏈

相關文章