JavaScript真的挺無語的,怪不得看了那麼多的介紹文章還是一頭霧水,直到自己終於弄懂了一點點之後才深有體會:
先從整體說起吧,發現沒有基礎做依據,那都是空中樓閣;
先從基礎開始介紹吧,又發現基礎是個蛇頭咬蛇尾的圓環,無從下手,應該先整體介紹。
於是介紹本身就成了一個死迴圈。。。
還是嘗試著從基礎開始。。。(多圖預警)
主要內容:
- 物件的繼承樹。
- 函式的繼承樹。
- 函式 VS 物件
- prototype VS _ _ proto__
- 繼承 VS 組合
- 自己定義函式(class),以及實現繼承
尋找原型鏈的“源頭”
網上有一個梗:萬物基於MIUI。雖然是一句調侃,但是也表達源頭的重要性。
看過一些高手寫的關係圖,應該是非常專業,但也正是因為太專業了,所以才導致新手看的是一頭霧水。
那麼對於先手來說,有沒有簡單一點的方式呢?我們可以借鑑一下物件導向的思路。
提到物件導向,大家都會想到基類(超類、父類)、子類、繼承、多型等。為啥容易記住呢?因為繼承關係非常簡單,從基類開始,一層一層繼承下去,結構非常清晰明瞭。
我覺得應該借鑑一下這種表達方式,也許這種方式並不契合JavaScript,但是我覺得應該比較方便初學者入門。
經常聽說,JavaScript 的世界是基於 Object 的,這句話對但是又不對,為啥這麼說呢?我們來看看 Object 的結構:(使用 console.dir() 可以看到細節 )
console.dir(Object)
首先請注意一下那個 f 的標識,這表示 Object 其實是一個函式(從 JavaScript 的語法角度來看),我們來驗證一下:
這到底是怎麼回事呢?後面細說,先把找到源頭才好理解。
這個 Object 並不是源頭,因為還有 prototype 和 __ proto__, 我們先看看 Object.prototype 的結構:
Object.prototype
console.dir(Object.prototype)
可以看到,Object.prototype 才是源頭,因為 Object.prototype 沒有 prototype(當然沒有),_ _ proto__ 也是 null,我們來驗證一下:
console.dir(Object.prototype.prototype)
console.dir(Object.prototype.__proto__)
Object.__proto __
這是啥?是不是很頭暈,這個其實指向的是 Function的原型,我們來驗證一下:
這是咋回事?後面再解釋。
小結
是不是有點暈,讓我們來梳理一下思路:
如果看上面的圖有點暈的話,可以先看下面的圖,灰線說的是建構函式的關係,可以先跳過。(終於畫出來了那種繞圈圈的圖,向著專業又邁出了一步)
-
思路一:
Object有兩個屬性,一個是物件原型,一個是函式原型。 -
思路二:
Object有兩個指標,一個指向物件原型,一個指向函式原型。
我覺得思路二更適合一些,這個是理解 JavaScript 的原型鏈的第一個門檻,如果繞不清楚的話……沒關係,往下看就好,我也是把下面都寫出來,然後回頭才整理出來這個圖的。。。(這個也是給繼承和組合做個鋪墊)
構建一顆大樹 —— 物件的繼承關係
找到源頭之後,我們就可以構建一顆大樹了。
構建原則:xxx.prototype._ _ proto__ === Object.prototype
即:Object.prototype 看做父類,然後把其“子類”畫出來。
這下是不是清晰多了呢?我們來驗證一下:
- Array
- String:
好長好長,差點截不下來。
- Number
- BigInt
- Boolean
- Symbol
- Date
- RegExp (正規表示式)
- Math
共同點
每種型別都有自己的成員,然後_ _ proto__ 指向 Object.prototype。
特例
這裡有幾個特殊情況:
-
Math
沒有原型,或者說原型就是 Math 自己。 -
Array
這個比較奇怪。 -
null 和 undefined
這對兄弟先當做特殊情況來處理。 -
Function
Function.prototype._ _ proto__ 也是指向 Object.prototype的,但是 Function.prototype 是一個 f。 -
Object
如果說 Object.prototype 是基類的話,那麼Object是啥呢?其實 Object 是函式。
是不是有點暈?從JavaScript 語法的角度來說,不僅 Object 是函式,String、Number這些都是函式。
再構建一顆大樹 —— 函式的繼承關係
觀察上面的圖(物件的樹)可以發現,我寫的都是xxx.prototype 的形式,那為啥不直接寫xxx呢?
因為從 JavaScript 的語法的角度來看,Object、String、Number、Array、Function等都是函式,Object.prototype、String.prototype 等才是物件。
我們從函式的角度來構造另一顆大樹。
依據:xxx._ _ proto__ === Function.prototype
即:把Function.prototype看做父類,把他的子類(__ proto__指向他的)都畫出來。
這裡加上“()”,明確一下,然後我們來看一下具體的結構:
- Function
- String
- Number
- Boolean
- BigInt
- Symbol
- Date
- RegExp
- Array
物件 VS 函式
物件和函式的樹都畫完了,然後我們來分析一下物件和函式的區別。
- 物件:是一個容器,可以存放各種型別的例項(資料),包括函式。
- 建構函式:依據原型建立原型的例項。(個人理解可能有誤)
- 一般函式:就是我們“隨手”寫的函式,執行某些程式碼,返回結果(也可以不返回)。
從 JavaScript 的語法角度來看,Object、Function、String、Date、Number等都是function,而Object.prototype、String.prototype、Date.prototype、Number.prototype等才是物件。
這和我們經常用到表述方式有點不一樣,也正是這個原因,導致理解和表達的時候非常混亂。
我們還是來驗證一下:
- 函式
- 物件
這裡有一個特例,Function.prototype 是一個函式,而且是所有函式的源頭。
所以說,從 JavaScript 的語法角度來看,函式就是函式,物件就是物件,不存在Object既是物件又是函式的情況。
那麼到底是什麼關係呢?我們定義一套函式來具體分析一下。
實戰:用ES6的class定義一套物件/函式
ES6提供了class,但是這個並不是類,而是 Function 的語法糖。
目的是簡化ES5裡面,為了實現繼承而採用的各種“神操作”。
用class來定義,結構和關係會非常清晰,再也不會看著頭疼了,建議新手可以跳過ES5的實現方式,直接用ES6的方式。
我們先定義一個Base,然後定義一個Person繼承Base,再定義一個Man繼承Person。
也就是說,可以深層繼承。
class Base {
constructor (title) {
this.title = '測試一下基類:' + title
}
baseFun1(info) {
console.log('\n這是base的函式一,引數:', info, '\nthis:', this)
}
}
class Person extends Base{
constructor (title, age) {
super(title)
this.title = '人類:' + title
this.age = age
}
personFun1(info) {
console.log('\n這是base的函式一,引數:', 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)
}
}
我們列印來看看結構:
-
建構函式 constructor
列印結果很清晰的表達了,建構函式就是我們定義的class。 -
屬性
屬性比較簡單,統統都掛在 this 上面,而且是同一個級別。 -
函式
函式就有點複雜了,首先函式是分級別的,掛在每一級的原型上面。
Base的函式(baseFun1),掛在Base的原型上面,_ _ proto__ 指向原型。
Person的函式(PersonFun1),應該掛在Person的原型上面,但是列印結果似乎是,Base好像標錯了位置。 -
原型鏈
Man的例項 > Man的原型 > Person的原型 > Base 的原型 > Object 的原型。
通過 _ _ proto__ 連線了起來。
Man的例項 man1,可以通過這個“鏈條”,找到 baseFun1,直接用即可(man1.baseFun1()
✔),
而不需要使用_ _ proto__(man1.__ proto__.__ proto__.__ proto__.baseFun1()
✘)
這個和麵對物件的繼承是一樣的效果。
prototype VS _ _ proto _ _
看上面兩個大樹,既有 prototype 又有 _ _ proto _ _,好亂的樣子。那麼怎麼辦呢?我們可以找一下規律:
-
prototype:
prototype 是自己的原型,可以其原型可以是函式,也可以是物件。有各自的繼承體系。 -
__ proto __ :
__ proto __ 指向上一級原型,並不是自己的,只是一個指標,方便使用父級的方法和屬性。
可以指向物件,也可以指向函式。
組合 VS 繼承
一提到物件導向,大家一般都會想到封裝、繼承、和多型。但是 JavaScript 卻不是這個思路。
上面那顆大樹看起來是繼承的關係,Object.prototype 是基類,派生出來 Object.prototype、Function.prototype、string.prototype等。
但是其實這裡面隱藏了組合的方式。
我們展開Object 來看看,就會發現自己進入了一個迷宮
object 的“特殊”的結構
看上面的圖我們會發現,Object 並不像我們想象的那麼簡單,有很多的方法,我們隨便找幾個點開看看:
每一個函式都有自己的原型鏈(__ proto__),是不是有一種進入迷宮的感覺?我當初看的到時候就被嚇退了,這都是個啥?
但是我們換個思路來理解,就清晰多了,那就是:組合代替繼承!
Object 其實是由若干個函式組合而成。
其實,想一想,JavaScript 沒有私有成員,所以各種細節都暴露出來了,所以我們可以看到原型,看到原型鏈,看到建構函式。
正是因為看到了這麼多的細節,而以前又沒有一個比較好的封裝方式,所以看起來就特別的亂,理解起來也特別頭疼。
總結
按照 JavaScript 的語法來總結,否則總感覺說不清楚。
-
物件(xxx.prototype)
- 物件的“根”是 Object.prototype,其上一級是null。
- 物件只有_ _ proto__,指向上一級原型。
- 物件沒有 prototype,因為Object.prototype、String.prototype、Number.prototype等本身就是物件。通過 _ _ proto__尋找上一級原型。
-
函式
- 函式的“根”是 Function.prototype,其上一級是 Object.prototype。
- 函式有prototype,(JavaScript 語法角度)Object、String、Function、Number 等都是函式,同時也是其原型的建構函式。
- 函式有_ _ proto__,指向上一級函式。
-
例項只有_ _ proto__ ,指向函式原型。