JavaScript真的挺無(wú)語(yǔ)的,怪不得看了那么多的介紹文章還是一頭霧水,直到自己終于弄懂了一點(diǎn)點(diǎn)之后才深有體會(huì):
先從整體說(shuō)起吧,發(fā)現(xiàn)沒(méi)有基礎(chǔ)做依據(jù),那都是空中樓閣;
先從基礎(chǔ)開(kāi)始介紹吧,又發(fā)現(xiàn)基礎(chǔ)是個(gè)蛇頭咬蛇尾的圓環(huán),無(wú)從下手,應(yīng)該先整體介紹。
于是介紹本身就成了一個(gè)死循環(huán)。。。
還是嘗試著從基礎(chǔ)開(kāi)始。。。(多圖預(yù)警)
主要內(nèi)容:
- 對(duì)象的繼承樹(shù)。
- 函數(shù)的繼承樹(shù)。
- 函數(shù) VS 對(duì)象
- prototype VS _ _ proto__
- 繼承 VS 組合
- 自己定義函數(shù)(class),以及實(shí)現(xiàn)繼承
尋找原型鏈的“源頭”
網(wǎng)上有一個(gè)梗:萬(wàn)物基于MIUI。雖然是一句調(diào)侃,但是也表達(dá)源頭的重要性。
看過(guò)一些高手寫(xiě)的關(guān)系圖,應(yīng)該是非常專業(yè),但也正是因?yàn)樘珜I(yè)了,所以才導(dǎo)致新手看的是一頭霧水。
那么對(duì)于先手來(lái)說(shuō),有沒(méi)有簡(jiǎn)單一點(diǎn)的方式呢?我們可以借鑒一下面向?qū)ο蟮乃悸贰?/p>
提到面向?qū)ο?,大家都?huì)想到基類(超類、父類)、子類、繼承、多態(tài)等。為啥容易記住呢?因?yàn)槔^承關(guān)系非常簡(jiǎn)單,從基類開(kāi)始,一層一層繼承下去,結(jié)構(gòu)非常清晰明了。
我覺(jué)得應(yīng)該借鑒一下這種表達(dá)方式,也許這種方式并不契合JavaScript,但是我覺(jué)得應(yīng)該比較方便初學(xué)者入門。
經(jīng)常聽(tīng)說(shuō),JavaScript 的世界是基于 Object 的,這句話對(duì)但是又不對(duì),為啥這么說(shuō)呢?我們來(lái)看看 Object 的結(jié)構(gòu):(使用 console.dir() 可以看到細(xì)節(jié) )
console.dir(Object)

首先請(qǐng)注意一下那個(gè) f 的標(biāo)識(shí),這表示 Object 其實(shí)是一個(gè)函數(shù)(從 JavaScript 的語(yǔ)法角度來(lái)看),我們來(lái)驗(yàn)證一下:

這到底是怎么回事呢?后面細(xì)說(shuō),先把找到源頭才好理解。
這個(gè) Object 并不是源頭,因?yàn)檫€有 prototype 和 __ proto__, 我們先看看 Object.prototype 的結(jié)構(gòu):
Object.prototype
console.dir(Object.prototype)

可以看到,Object.prototype 才是源頭,因?yàn)?Object.prototype 沒(méi)有 prototype(當(dāng)然沒(méi)有),_ _ proto__ 也是 null,我們來(lái)驗(yàn)證一下:
console.dir(Object.prototype.prototype)
console.dir(Object.prototype.__proto__)


Object.__proto __

這是啥?是不是很頭暈,這個(gè)其實(shí)指向的是 Function的原型,我們來(lái)驗(yàn)證一下:

這是咋回事?后面再解釋。
小結(jié)
是不是有點(diǎn)暈,讓我們來(lái)梳理一下思路:

如果看上面的圖有點(diǎn)暈的話,可以先看下面的圖,灰線說(shuō)的是構(gòu)造函數(shù)的關(guān)系,可以先跳過(guò)。(終于畫(huà)出來(lái)了那種繞圈圈的圖,向著專業(yè)又邁出了一步)

思路一:
Object有兩個(gè)屬性,一個(gè)是對(duì)象原型,一個(gè)是函數(shù)原型。思路二:
Object有兩個(gè)指針,一個(gè)指向?qū)ο笤?,一個(gè)指向函數(shù)原型。
我覺(jué)得思路二更適合一些,這個(gè)是理解 JavaScript 的原型鏈的第一個(gè)門檻,如果繞不清楚的話……沒(méi)關(guān)系,往下看就好,我也是把下面都寫(xiě)出來(lái),然后回頭才整理出來(lái)這個(gè)圖的。。。(這個(gè)也是給繼承和組合做個(gè)鋪墊)
構(gòu)建一顆大樹(shù) —— 對(duì)象的繼承關(guān)系
找到源頭之后,我們就可以構(gòu)建一顆大樹(shù)了。
構(gòu)建原則:xxx.prototype._ _ proto__ === Object.prototype
即:Object.prototype 看做父類,然后把其“子類”畫(huà)出來(lái)。

這下是不是清晰多了呢?我們來(lái)驗(yàn)證一下:
- Array

- String:

好長(zhǎng)好長(zhǎng),差點(diǎn)截不下來(lái)。
- Number

- BigInt

- Boolean

- Symbol

- Date

- RegExp (正則表達(dá)式)

- Math

共同點(diǎn)
每種類型都有自己的成員,然后_ _ proto__ 指向 Object.prototype。
特例
這里有幾個(gè)特殊情況:
Math
沒(méi)有原型,或者說(shuō)原型就是 Math 自己。Array
這個(gè)比較奇怪。null 和 undefined
這對(duì)兄弟先當(dāng)做特殊情況來(lái)處理。Function
Function.prototype._ _ proto__ 也是指向 Object.prototype的,但是 Function.prototype 是一個(gè) f。Object
如果說(shuō) Object.prototype 是基類的話,那么Object是啥呢?其實(shí) Object 是函數(shù)。
是不是有點(diǎn)暈?從JavaScript 語(yǔ)法的角度來(lái)說(shuō),不僅 Object 是函數(shù),String、Number這些都是函數(shù)。
再構(gòu)建一顆大樹(shù) —— 函數(shù)的繼承關(guān)系
觀察上面的圖(對(duì)象的樹(shù))可以發(fā)現(xiàn),我寫(xiě)的都是xxx.prototype 的形式,那為啥不直接寫(xiě)xxx呢?
因?yàn)閺?JavaScript 的語(yǔ)法的角度來(lái)看,Object、String、Number、Array、Function等都是函數(shù),Object.prototype、String.prototype 等才是對(duì)象。
我們從函數(shù)的角度來(lái)構(gòu)造另一顆大樹(shù)。
依據(jù):xxx._ _ proto__ === Function.prototype
即:把Function.prototype看做父類,把他的子類(__ proto__指向他的)都畫(huà)出來(lái)。

這里加上“()”,明確一下,然后我們來(lái)看一下具體的結(jié)構(gòu):
- Function

- String

- Number

- Boolean

- BigInt

- Symbol

- Date

- RegExp

- Array

對(duì)象 VS 函數(shù)
對(duì)象和函數(shù)的樹(shù)都畫(huà)完了,然后我們來(lái)分析一下對(duì)象和函數(shù)的區(qū)別。
- 對(duì)象:是一個(gè)容器,可以存放各種類型的實(shí)例(數(shù)據(jù)),包括函數(shù)。
- 構(gòu)造函數(shù):依據(jù)原型創(chuàng)建原型的實(shí)例。(個(gè)人理解可能有誤)
- 一般函數(shù):就是我們“隨手”寫(xiě)的函數(shù),執(zhí)行某些代碼,返回結(jié)果(也可以不返回)。
從 JavaScript 的語(yǔ)法角度來(lái)看,Object、Function、String、Date、Number等都是function,而Object.prototype、String.prototype、Date.prototype、Number.prototype等才是對(duì)象。
這和我們經(jīng)常用到表述方式有點(diǎn)不一樣,也正是這個(gè)原因,導(dǎo)致理解和表達(dá)的時(shí)候非?;靵y。
我們還是來(lái)驗(yàn)證一下:
- 函數(shù)

- 對(duì)象

這里有一個(gè)特例,F(xiàn)unction.prototype 是一個(gè)函數(shù),而且是所有函數(shù)的源頭。
所以說(shuō),從 JavaScript 的語(yǔ)法角度來(lái)看,函數(shù)就是函數(shù),對(duì)象就是對(duì)象,不存在Object既是對(duì)象又是函數(shù)的情況。
那么到底是什么關(guān)系呢?我們定義一套函數(shù)來(lái)具體分析一下。
實(shí)戰(zhàn):用ES6的class定義一套對(duì)象/函數(shù)
ES6提供了class,但是這個(gè)并不是類,而是 Function 的語(yǔ)法糖。
目的是簡(jiǎn)化ES5里面,為了實(shí)現(xiàn)繼承而采用的各種“神操作”。
用class來(lái)定義,結(jié)構(gòu)和關(guān)系會(huì)非常清晰,再也不會(huì)看著頭疼了,建議新手可以跳過(guò)ES5的實(shí)現(xiàn)方式,直接用ES6的方式。
我們先定義一個(gè)Base,然后定義一個(gè)Person繼承Base,再定義一個(gè)Man繼承Person。
也就是說(shuō),可以深層繼承。
class Base {
constructor (title) {
this.title = '測(cè)試一下基類:' + title
}
baseFun1(info) {
console.log('\n這是base的函數(shù)一,參數(shù):', info, '\nthis:', this)
}
}
class Person extends Base{
constructor (title, age) {
super(title)
this.title = '人類:' + title
this.age = age
}
personFun1(info) {
console.log('\n這是base的函數(shù)一,參數(shù):', info, '\nthis:', this)
}
}
class Man extends Person {
constructor (title, age, date) {
super(title, age)
this.title = '男人:' + title
this.birthday = date
}
manFun3 (msg) {
console.log('jim 的 this ===', this, msg)
}
}
我們打印來(lái)看看結(jié)構(gòu):

構(gòu)造函數(shù) constructor
打印結(jié)果很清晰的表達(dá)了,構(gòu)造函數(shù)就是我們定義的class。屬性
屬性比較簡(jiǎn)單,統(tǒng)統(tǒng)都掛在 this 上面,而且是同一個(gè)級(jí)別。函數(shù)
函數(shù)就有點(diǎn)復(fù)雜了,首先函數(shù)是分級(jí)別的,掛在每一級(jí)的原型上面。
Base的函數(shù)(baseFun1),掛在Base的原型上面,_ _ proto__ 指向原型。
Person的函數(shù)(PersonFun1),應(yīng)該掛在Person的原型上面,但是打印結(jié)果似乎是,Base好像標(biāo)錯(cuò)了位置。原型鏈
Man的實(shí)例 > Man的原型 > Person的原型 > Base 的原型 > Object 的原型。
通過(guò) _ _ proto__ 連接了起來(lái)。
Man的實(shí)例 man1,可以通過(guò)這個(gè)“鏈條”,找到 baseFun1,直接用即可(man1.baseFun1()?),
而不需要使用_ _ proto__(man1.__ proto__.__ proto__.__ proto__.baseFun1()?)
這個(gè)和面對(duì)對(duì)象的繼承是一樣的效果。
prototype VS _ _ proto _ _
看上面兩個(gè)大樹(shù),既有 prototype 又有 _ _ proto _ _,好亂的樣子。那么怎么辦呢?我們可以找一下規(guī)律:
prototype:
prototype 是自己的原型,可以其原型可以是函數(shù),也可以是對(duì)象。有各自的繼承體系。__ proto __ :
__ proto __ 指向上一級(jí)原型,并不是自己的,只是一個(gè)指針,方便使用父級(jí)的方法和屬性。
可以指向?qū)ο?,也可以指向函?shù)。

組合 VS 繼承
一提到面向?qū)ο螅蠹乙话愣紩?huì)想到封裝、繼承、和多態(tài)。但是 JavaScript 卻不是這個(gè)思路。
上面那顆大樹(shù)看起來(lái)是繼承的關(guān)系,Object.prototype 是基類,派生出來(lái) Object.prototype、Function.prototype、string.prototype等。
但是其實(shí)這里面隱藏了組合的方式。
我們展開(kāi)Object 來(lái)看看,就會(huì)發(fā)現(xiàn)自己進(jìn)入了一個(gè)迷宮

object 的“特殊”的結(jié)構(gòu)
看上面的圖我們會(huì)發(fā)現(xiàn),Object 并不像我們想象的那么簡(jiǎn)單,有很多的方法,我們隨便找?guī)讉€(gè)點(diǎn)開(kāi)看看:

每一個(gè)函數(shù)都有自己的原型鏈(__ proto__),是不是有一種進(jìn)入迷宮的感覺(jué)?我當(dāng)初看的到時(shí)候就被嚇退了,這都是個(gè)啥?
但是我們換個(gè)思路來(lái)理解,就清晰多了,那就是:組合代替繼承!
Object 其實(shí)是由若干個(gè)函數(shù)組合而成。
其實(shí),想一想,JavaScript 沒(méi)有私有成員,所以各種細(xì)節(jié)都暴露出來(lái)了,所以我們可以看到原型,看到原型鏈,看到構(gòu)造函數(shù)。
正是因?yàn)榭吹搅诉@么多的細(xì)節(jié),而以前又沒(méi)有一個(gè)比較好的封裝方式,所以看起來(lái)就特別的亂,理解起來(lái)也特別頭疼。
總結(jié)
按照 JavaScript 的語(yǔ)法來(lái)總結(jié),否則總感覺(jué)說(shuō)不清楚。
-
對(duì)象(xxx.prototype)
- 對(duì)象的“根”是 Object.prototype,其上一級(jí)是null。
- 對(duì)象只有_ _ proto__,指向上一級(jí)原型。
- 對(duì)象沒(méi)有 prototype,因?yàn)镺bject.prototype、String.prototype、Number.prototype等本身就是對(duì)象。通過(guò) _ _ proto__尋找上一級(jí)原型。
-
函數(shù)
- 函數(shù)的“根”是 Function.prototype,其上一級(jí)是 Object.prototype。
- 函數(shù)有prototype,(JavaScript 語(yǔ)法角度)Object、String、Function、Number 等都是函數(shù),同時(shí)也是其原型的構(gòu)造函數(shù)。
- 函數(shù)有_ _ proto__,指向上一級(jí)函數(shù)。
實(shí)例只有_ _ proto__ ,指向函數(shù)原型。