前言
該系列文章將帶你全面理解js物件和原型鏈,並用es5去實現類以及認識es6中class
的美妙。該系列一共有3篇文章:
- 從
__proto__
和prototype
來深入理解JS物件和原型鏈 - 用
javascript
實現類與繼承 - 初始
ES6
的class
此篇為第一篇從__proto__
和prototype
來深入理解JS物件和原型鏈
prototype
和__proto__
何為原型
引用《JavaScript權威指南》的一段描述
Every JavaScript object has a second JavaScript object (or null ,
but this is rare) associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.
複製程式碼
翻譯過來就是:每一個JS物件一定關聯著另外一個JS物件(也許是null
,但是它一定是唯一的)。這個另外的物件就是所謂的原型物件。每個物件從它的原型物件中繼承屬性和方法。
如果你是初學者,這句話肯定很繞吧(不過它確實描述得很精闢)。沒關係,你只需要先把握以下兩點就好了:
-
在JS裡,萬物皆物件。方法(
Function
)是一個物件,方法的原型(Function.prototype
)是物件。 -
JS有三種構造物件的方式
-
通過物件字面量
var person1 = { name: 'Jzin', sex: 'male' } 複製程式碼
-
通過建構函式
function Person(name, sex) { this.name = name; this.sex = sex; } var person1 = new Person('Jzin', 'male'); 複製程式碼
所謂建構函式就是:可以通過它來new出一個物件例項的函式。通常建構函式裡都用了
this
,因為這樣子才會給呼叫它的物件繫結屬性。 -
由函式
Object.create
構造var person1 = { name: 'Jzin', sex: 'male' } var person2 = Object.create(person1); 複製程式碼
這三種方法的異同到後面還會繼續分析,現在你只需掌握如何構建物件就好啦。
-
以上兩點就是這小結你要掌握的東西:
- 萬物皆物件的思想
- 如何構造物件
至此,我還沒介紹什麼是原型,不過沒關係,我們先看看原型的分類,慢慢你就會理解了。
原型的分類
JS的原型分成兩類:隱式原型和顯示原型
顯式原型(explicit prototype property)
當你建立一個函式時,JS會為這個函式(別忘了:JS一切皆物件)自動新增prototype
屬性,這個屬性的值是一個物件,也就是原型物件(即函式名.prototype
)。這個物件並不是空物件,它擁有constructor
屬性,屬性值就是原函式。當然,你也可以自己在原型物件中新增你需要的屬性(即函式名.prototype.屬性名=屬性值
).
那麼原型物件(prototype
)的作用是什麼呢?可以用原型物件來實現繼承,即通過函式構造出來的例項可以直接訪問其建構函式的原型物件中的屬性。可能有點繞,但是讀到後面你就會理解啦。
需要注意的是:
- 顯式原型(
prototype
)只有函式才擁有。我們後面講的隱式原型則是所有物件都有。 - 通過
Function.prototype.bind
方法構造出來的函式是個例外,它沒有prototype
屬性。
隱式原型( implicit prototype link)
JavaScript中任意物件都有一個內建屬性[[prototype]],在ES5之前沒有標準的方法訪問這個內建屬性,但是大多數瀏覽器都支援通過__proto__
來訪問。現在,所謂的隱式原型就是__proto__
了。
-
隱式原型的指向
隱式原型指向建立這個物件的函式的
prototype
(Object.create
函式構造出來的例項有點例為,後面會說明。其實也不是例為,只是它經過了一定的封裝)。看下面的例子function person(name) { this.name = name; } person.prototype.class = 'Human'; var person1 = new person('Jzin'); console.log(person1.__proto__); //person { class: 'Human' } console.log(person.__proto__); //[Function] 複製程式碼
person1
的__proto__
很容易理解:它是由person
方法構造的例項,它的__proto
自然就是person.prototype
person
的__proto__
呢?其實每一個方法的構造方法都是Function
方法,也就是所有方法的__proto__
都是Function.prototype
。如果現在還不理解也沒關係,後面會有一副圖幫你理解。 -
隱式原型的作用
- 構成原型鏈,同樣用於實現基於原型的繼承。舉個例子,當我們訪問obj這個物件中的x屬性時,如果在obj中找不到,那麼就會沿著
__proto__
依次查詢。這也是protorype
可以實現繼承的原因。 - 可以用來判斷一個物件(L)是否是某個函式(R)的例項:只需判斷
L.__proto__.__proto__ ..... === R.prototype
這個是否為真就行了。這也是instanceof
運算子的原理。後面會講到。
- 構成原型鏈,同樣用於實現基於原型的繼承。舉個例子,當我們訪問obj這個物件中的x屬性時,如果在obj中找不到,那麼就會沿著
一張圖帶你形象理解__proto__
和prototype
先上圖,如果圖片顯示不了可以點選這裡:傳送門。
我們來理解一下這幅圖:
- 建構函式
Foo()
- 建構函式
Foo
的原型屬性prototype
指向了它的原型物件Foo.prototype
。原型物件Foo.protoype
中有預設屬性constructor
指向了原函式Foo
。 - 建構函式
Foo
建立的例項f2,f1
的__proto__
指向了其建構函式的原型物件Foo.prototype
,所以Foo
的所有例項都可以共享其原型物件的屬性。 - 建構函式
Foo
其實是Function
函式建立的例項物件,所以它的__proto__
就是Function
函式的原型物件Function.prototype
。 - 建構函式
Foo
的原型物件其實是Object
函式建立的例項物件,所以它的__proto__
就是Object
函式的原型物件Object.prototype
。
- 建構函式
- Funtion函式
- 你所寫的所有函式,其實都是
Function
函式構造的例項物件。所以所有函式的__proto__
都指向Fucntion.prototype
。 Function
函式物件是由它本身建立(姑且可以這麼理解),所以Function.__proto__d等
於Function_prototype
Function
函式的原型物件其實是Object
函式建立的例項物件,所以它的__proto__
就是Object
函式的原型物件Object.prototype
。
- 你所寫的所有函式,其實都是
- Object函式
Object
函式其實是Function
函式建立的例項物件,所以它的__proto__
就是Function
函式的原型物件Function.prototype
。- 需要注意的是:
Object.prototype
的__proto__
是指向null
的!!!
相信你通過這幅圖,已經對原型有自己的理解了,我們來總結一下:
- 物件有
__proto__
屬性,指向該物件的建構函式的原型物件。 - 方法除了有
__proto__
屬性,還有prototype
屬性,prototype
指向該方法的原型物件。
深入理解__proto__
的指向
相信經過上面的介紹,你已經能很好地掌握__proto__
的指向了。本節通過一些實際的例子讓你更加深入地理解__proto__
的指向。
在一開始,我們瞭解了構造物件的三種方式:(1)物件字面量的方式 (2)new的方式 (3)ES5中的Object.create()。其實,這三種方式在我看來都是一種的,即通過new來構建。為什麼這麼說呢?我們來仔細分析分析:
-
通過字面量構造物件
var person1 = { name: 'Jzin', sex: 'male' } 複製程式碼
其實這種方式只是為了開發人員更方便建立物件的一個語法糖(語法糖:顧名思義,就是很甜的糖,經過程式碼封裝,讓語法更加人性化,實際的內部實現是一樣的)。
上面也就等價於:
var person1 = new Object(); person1.name = 'Jzin'; person1.sex = 'male'; 複製程式碼
person1
是Object
函式構造的物件,所以person1.__ptoto__
就指向Object.prototype
。也就是說,通過物件字面量構造出來的物件,其
__proto__
都是指向Object.prototype
-
通過建構函式
function Person(name, sex) { this.name = name; this.sex = sex; } var person1 = new Person('Jzin', 'male'); 複製程式碼
通過new操作符呼叫的函式就是建構函式。由建構函式構造的物件,其
__proto__
指向其建構函式的原型物件。在本例中,
person1.__proto__
就指向Person.prototype
。 -
由函式
Object.create
構造var person1 = { name: 'Jzin', sex: 'male' } var person2 = Object.create(person1); 複製程式碼
由函式
Object.create(obj)
構造出來的物件,其隱式原型有點特殊:指向obj.prototype
。在本例中,person2.__proto__
指向person1
。這是為什麼呢?我們來分析一下。在沒有
Object.create
函式的日子裡,為了實現這一功能,我們需要這樣子做:Object.create = function(p) { function F(){} F.prototype = p; return new F(); } var f = Object.create(p); 複製程式碼
這樣子也就是實現了其功能,分析如下:
// 以下是用於驗證的虛擬碼 var f = new F(); //var f = Object.create(p); // 於是有 f.__proto__ === F.prototype //true // 又因為 F.prototype === p; //true // 所以 f.__proto__ === o //true 複製程式碼
因此由
Object.create(p)
建立出來的物件它的隱式原型指向p。
通過上面的分析,相信你對原型又進一步理解啦。我們再來幾題玩玩。
-
建構函式的顯式原型的隱式原型
-
內建物件(built-in object)的的隱式原型
比如
Array()
,Array.prototype.__proto__
指向什麼?Array.prototype.__proto__ === Object.prototype //true 複製程式碼
比如
Function()
,Function.prototype.__proto__
指向什麼?Function.prototype.__proto__ === Object.prototype //true 複製程式碼
根據上面那幅圖,這些也很簡單啦。
-
-
自定義物件
-
預設情況下
function Foo(){} var foo = new Foo() Foo.prototype.__proto__ === Object.prototype //true foo.prototype.__proto__ === Foo.prototype //true 複製程式碼
理由,就不必解釋了吧
-
其他情況
-
function Bar(){} function Foo(){} //這時我們想讓Foo繼承Bar Foo.prototype = new Bar() Foo.prototype.__proto__ === Bar.prototype //true console.log(Foo.prototype.constructor); //[Function: Bar] 複製程式碼
-
function Foo(){} //我們不想讓Foo繼承誰,但是我們要自己重新定義Foo.prototype Foo.prototype = { a:10, b:-10 } //這種方式就是用了物件字面量的方式來建立一個物件,根據前文所述 Foo.prototype.__proto__ === Object.prototype console.log(Foo.prototype.constructor); //[Function: Object] 複製程式碼
注意:以上兩種情況都等於完全重寫了
Foo.prototype
,所以Foo.prototype.constructor
也跟著改變了,於是constructor
這個屬性和原來的建構函式Foo
也就切斷了聯絡。 -
-
instanceof
instanceof
的左值一般是一個物件,右值一般是一個建構函式,用來判斷左值是否是右值的例項。instanceof
操作符的內部實現機制和隱式原型、顯式原型有直接的關係,它的內部實現原理是這樣的:
//設 L instanceof R
//通過判斷
L.__proto__.__proto__ ..... === R.prototype ?
//最終返回true or false
複製程式碼
也就是沿著L的__proto__
一直尋找到原型鏈末端,直到等於R.prototype
為止。知道了這個也就知道為什麼以下這些奇怪的表示式為什麼會得到相應的值了
Function instanceof Function //true
Function instanceof Object // true
Object instanceof Function // true
Object instanceof Object // true
Number instanceof Number //false
Number instanceof Function //true
Number instanceof Object //true
複製程式碼
你發現沒有:這就是原型鏈啊!!!
L1.__proto__
指向R1.prototype
,
R1.prototype.__proto__
指向R2.prototype
...
Rn.prototype.__proto__
指向Object.prototype
Object.prototype.__proto__
指向null
這樣子就把原型串起來啦,也就是實現了繼承。也就是為什麼所有物件都要toString
方法,因為這個方法在Object.prototype
上面啊啊啊啊。
總結
至此,相信你已經完全理解原型和原型鏈了。當然,只是理解不實踐是沒用的。在下一篇,我們將利用原型來實現類與繼承。