JavaScript 中的原型也是一個(gè)非常讓人頭疼的東西,很多前端同學(xué)對此也是一知半解,比如我。今天我們就好好捋一捋這個(gè)原型。
創(chuàng)建對象的方式
下面就是創(chuàng)建對象的幾種方式:
var o1 = {
a: 123,
b: 'hello world'
}
console.log(o1.b)
function fun2() {
this.a = 33
this.b = 'hello o2'
}
var o2 = new fun2()
console.log(o2.b)
class Fun3 {
constructor() {
this.a = 365
this.b = 'hello class'
}
}
var o3 = new Fun3()
console.log(o3.b)
有人說這是三種創(chuàng)建方式,但是我認(rèn)為其實(shí)是兩種創(chuàng)建方式(因?yàn)?class 語法糖的本質(zhì)還是 function):直接定義對象和使用 new 關(guān)鍵詞構(gòu)造對象。
原型和原型鏈
當(dāng)我們創(chuàng)建了一個(gè)對象之后,就產(chǎn)生了原型(Object.create(null) 是特例)。
prototype 和 __proto__ 的區(qū)別
__proto__ 是一個(gè)非正式的屬性,很多環(huán)境中不支持該屬性。它指向當(dāng)前對象的原型。如下圖:

上面的代碼是一段原型繼承,可以看到對象 obj1 繼承了對象 obj,所以 obj1 的
__proto__ 就指向了 obj,而 obj 的 __proto__ 則指向了 Object。所有對象的原型鏈最終都將指向 Object。
而關(guān)于 prototype 我摘錄了一段話:
當(dāng)你創(chuàng)建函數(shù)時(shí),JS 會為這個(gè)函數(shù)自動(dòng)添加
prototype屬性,值是一個(gè)有 constructor 屬性的對象。而一旦你把這個(gè)函數(shù)當(dāng)作構(gòu)造函數(shù)(constructor)調(diào)用(即通過new關(guān)鍵字調(diào)用),那么 JS 就會幫你創(chuàng)建該構(gòu)造函數(shù)的實(shí)例,實(shí)例繼承構(gòu)造函數(shù)prototype的所有屬性和方法。

可以看到,對象 bar 的 __proto__ 屬性指向了函數(shù) func 的 prototype。
總結(jié)下,__proto__ 指向原型,而 prototype 是函數(shù)獨(dú)有且構(gòu)造的對象原型指向 prototype。
理解原型鏈
每個(gè)對象都是原型,而對象之間是可以繼承的。所以就產(chǎn)生了原型鏈??磮D說話:

很好理解了,我們創(chuàng)建了四個(gè)對象逐層進(jìn)行原型繼承。最后打印 obj3 對象可以看到 obj3 -> obj2 -> obj1 -> obj -> Object 這就是原型鏈。
如果我要在 obj3 對象上訪問 a 屬性,那么 JavaScript 就會順著原型鏈逐層往下找,最終在 obj 對象上找到了a 屬性,這就是原型鏈查找數(shù)據(jù)的方式。如果找到 Object 也沒有找到屬性就返回 undefined。

為對象指定原型的兩種方式
那么如何為對象添加原型呢?
1. new 關(guān)鍵字
第一種就是通過構(gòu)造器的方式來創(chuàng)建。
function Foo () {
this.a = 11
this.b = 22
}
Foo.prototype.c = 33
Foo.prototype.func = () => {
console.log('hello')
}
var f = new Foo()
console.log(f)
console.log(Object.getPrototypeOf(f))
當(dāng)然,不得不說的是 ES6 的 class 語法糖寫法:
class Foo {
constructor() {
this.a = 11
this.b = 22
}
func() {
console.log('hello')
}
}
var f = new Foo()
兩者其實(shí)是一樣的效果,但是 class 寫法更接近常規(guī)的類寫法。(終于可以讓 function 回歸它原本的作用上了。)
2. Object.create(obj) 面向?qū)ο?/h2>
Object.create() 可以很好的實(shí)現(xiàn)原型繼承行為,也能通過 Object API 來修改原型:
var obj = { a: 123, b: 456 }
Object.setPrototypeOf(obj, { c: 789 })
var obj2 = Object.create(obj)
obj2.e = 555
代碼輸出結(jié)果如下圖,的確實(shí)現(xiàn)了為對象指定原型的行為。

引用流還是復(fù)制流?
使用 JavaScript 原型是特別要主義的一個(gè)點(diǎn)是:JavaScript 對于原型的繼承是一種引用行為,即所引用的對象改變,繼承對象的原型也會改變。
與之相反的,有些語言會使用復(fù)制的方式。即在原型繼承時(shí)復(fù)制一份原型到當(dāng)前對象,從此被復(fù)制的對象和復(fù)制對象再無瓜葛。
總結(jié)
隨著 Object.create() 等一系列新 API 和 ES6 的 class 寫法的出現(xiàn),使用 function 作為構(gòu)造器并使用 prototype 來修改原型的方式將逐漸被拋棄。但是由于歷史原因這部分知識還是要理解其中原理的。
而 __proto__ 屬性是非正式屬性,不適合在通用場景下使用。
而對于原型的寫法,我認(rèn)為有兩種不錯(cuò)的處理方式:
- 完全使用 class 構(gòu)造器寫法來替代使用 function 構(gòu)造器的寫法來進(jìn)行面向類的開發(fā)方式。
- 放棄原型寫法,使用 Object 系列 API 進(jìn)行面向?qū)ο?/strong>的開發(fā)(行為委托就是這樣的方式)。
最后
關(guān)于原型,先聊這么多。明天我們聊聊基于 Object API 來實(shí)現(xiàn)的面向?qū)ο竽J?—— 行為委托,敬請期待。