[值與類型]關(guān)于編程中的“量”和“值”——JavaScript視角
本文全文依cc0公開授權(quán)。?
https://creativecommons.org/choose/zero/?lang=zh
很多人,包括很多有相當(dāng)經(jīng)驗(yàn)的開發(fā)者/程序員,很容易混淆或忽略量和值的概念和區(qū)別,而這種混淆有時(shí),尤其是在某些編程語(yǔ)言中,會(huì)造成一些微妙的誤會(huì),因此恰當(dāng)?shù)脜^(qū)分這兩者是有意義的。
實(shí)現(xiàn)/物理/機(jī)器層面的“量”和“值”
這里的“機(jī)器層面”并不指實(shí)際的某種電子計(jì)算機(jī),而是指針對(duì)現(xiàn)在的計(jì)算機(jī)的抽象模型。實(shí)際上,我們?cè)趯?shí)踐中所討論的“機(jī)器層面”,經(jīng)常是“C編譯器和libc眼中的”機(jī)器層面,也就是C編譯器為程序員提供的對(duì)機(jī)器的抽象。
與圖靈機(jī)相似,這種模型中都存在一條紙帶,這條紙帶被劃分為一個(gè)一個(gè)的格子,而每個(gè)格子可以被寫入數(shù)字0或1。但與圖靈機(jī)不同,這條紙帶是被編址的并能夠被隨機(jī)訪問(wèn),也就是說(shuō)給定一個(gè)地址,“讀寫頭”可以立即轉(zhuǎn)到地址所指向的格子,而這條紙帶被叫做內(nèi)存。當(dāng)然,這都是小事。
另外,如果你使用過(guò)lisp機(jī)的話,那么是不是lisp機(jī)的機(jī)器層面會(huì)與C的機(jī)器層面大不相同呢?
在這樣的機(jī)器層面上,“量”指的是內(nèi)存的“格子”,而“值”指的是“格子中的內(nèi)容”:量是容器,值是內(nèi)容,這兩者的區(qū)分雖然相當(dāng)顯而易見,但也相當(dāng)容易被忽略。畢竟把“一杯水”區(qū)分為“一杯”和“水”似乎并沒有太大必要——雖然你大概從來(lái)沒有喝過(guò)玻璃。
但語(yǔ)言層面和機(jī)器層面是不同的
這實(shí)際上本來(lái)應(yīng)該是一件挺顯然的事情:編程語(yǔ)言提供了一系列抽象,不是為了讓程序員試圖去窺探和接觸底層邏輯的。但是C和C++卻似乎很喜歡培養(yǎng)黑客,這兩門語(yǔ)言中的很多概念都與底層實(shí)現(xiàn)有著清晰的對(duì)應(yīng),這在某種意義上是好的,但是這也讓很多人養(yǎng)成了某些微妙的習(xí)慣,然而這種清晰的對(duì)應(yīng)卻不是何時(shí)何地都存在的。
把編譯器視作魔法,雖然很多時(shí)候它并不是那么得魔法,至少能讓人輕松很多。當(dāng)然,也不是任何時(shí)候都必須把編譯器看作魔法,不然就沒有人再去維護(hù)編譯器了。
簡(jiǎn)單得說(shuō),標(biāo)識(shí)符本身即是量,而標(biāo)識(shí)符所指向的東西,無(wú)論它指向的是什么,是如何指向的,這個(gè)被指向的東西是值。與其說(shuō)量是容器而值是內(nèi)容,不如說(shuō)量是能指而值是所指,量是名稱而值是實(shí)體。
identifier和signifier的確很像呢...畢竟形式語(yǔ)言也是語(yǔ)言,語(yǔ)言學(xué)理論當(dāng)然也對(duì)形式語(yǔ)言有效。
比如在`let?a = 1`中,a是一個(gè)量,量a指向值1。
這里的“指向”是抽象意義上,或者說(shuō)語(yǔ)言意義上的指向。
而在`a = a + 1`中,則是計(jì)算(a+1)的值,令a重新指向它:“令a指向a+1”,而不是“令a加1”,才是這一語(yǔ)句在幾乎所有語(yǔ)言中的確切語(yǔ)義。
注意,此處只談?wù)Z義而不談實(shí)現(xiàn),實(shí)現(xiàn)是編譯器才需要關(guān)心的。
至少在JavaScript中,這并不是像C一樣的,a對(duì)應(yīng)了某一段內(nèi)存,而這一段內(nèi)存中的數(shù)字是1。所以請(qǐng)把JavaScript代碼看作是直接運(yùn)行在JavaScript引擎這一魔法之上的。
也是存在著某些語(yǔ)句具有“令a加1”這一語(yǔ)義的:at&t語(yǔ)法的`addq $1, %rax`便具“令rax加1”的語(yǔ)義。
機(jī)器/計(jì)算模型層面只允許0和1兩個(gè)值,但絕大多數(shù)語(yǔ)言允許的值比這些要多得多,包括0, 1, 2, 3,'a', 'b', c', d'以及'abcd',也包括自定義值,比如[1, 2, 3]和{a: 1, b: 2, c: 3},這些值被編譯器/引擎通過(guò)某種簡(jiǎn)單的魔法映射到了底層實(shí)現(xiàn),但是我們并不真的需要關(guān)心這一實(shí)現(xiàn)。
JavaScript視角
JavaScript引擎允許7種"原始類型"的值,外加僅此一種的非原始類型,Object。
雖然說(shuō)實(shí)際上某些Object值,就是說(shuō)Array和Function,對(duì)引擎有某種特權(quán),但總得來(lái)說(shuō),它們依然是Object。
注意,是7+1種“值”,而不是7+1種“量”。
為什么說(shuō)是7+1種值呢,比如說(shuō)
let a
定義了一個(gè)變量,而
a = 1
使得a指向了值1,或者
a = {a: 1}
使得a指向了值{a: 1}
其中1是number類型的值,而{a: 1}則是Object類型的值。
那么我們看到,a從來(lái)沒有關(guān)心過(guò)這些值屬于那些分類,而這些值卻天生帶有分類。所以,這些分類是為了劃分值而產(chǎn)生的,與量并沒有關(guān)系。
像大多數(shù)語(yǔ)言一樣,JavaScript中的值大都是不可變的,比如1永遠(yuǎn)都是1,某個(gè)量可能會(huì)從指向1變成指向2,但1永遠(yuǎn)不會(huì)變。在機(jī)器層面上,內(nèi)存中的某一位可能由0變?yōu)?,但是0這一數(shù)字本身永遠(yuǎn)不會(huì)是1。值是不變的,會(huì)變的只有指向,或者說(shuō)量和值的對(duì)應(yīng)關(guān)系。
不過(guò)JavaScript中有一類值卻是會(huì)變的,這就是被排除在7類primitive value之外的Object。
但是為什么要有一種會(huì)變的值呢?
比如說(shuō)你種下了一棵種子,把它命名為小花,過(guò)了幾天小花長(zhǎng)成了幼苗,你非常開心,無(wú)論是那顆種子還是這株幼苗,小花一直都是那個(gè)小花,但是它已經(jīng)不是你種下它的時(shí)候的樣子的。
Object就是這樣一顆能夠發(fā)芽的種子。
這種可變性使得Object能夠被用來(lái)做很多事情,不過(guò)也容易產(chǎn)生一些不太符合直覺的行為。
值的類型
我們知道值天然具有類型,比如1就是一個(gè)數(shù)字,而a就是一個(gè)字母。從數(shù)學(xué)上,我們可以把具有某一類性質(zhì)的值歸類于一個(gè)集合,這樣的集合就叫做類型。
當(dāng)然,值并不必須按照7種基本類型那樣劃分,這樣的劃分實(shí)際上是為了實(shí)現(xiàn)方便:因?yàn)椴煌愋偷闹敌枰煌膶?shí)現(xiàn),引擎將實(shí)現(xiàn)相同的值歸結(jié)為了同種類型:但是類型本身并不必然需要與實(shí)現(xiàn)掛鉤。
比如“0到255的整數(shù)”可以是一種類型,{0, 1, 2, 4}也可以是一種類型,換言之,只要集合論允許,有限或無(wú)限的值的任意一種集合都可以是一種類型,比如空集在很多種語(yǔ)言中都對(duì)應(yīng)void類型。
量的類型:靜態(tài)類型系統(tǒng)與動(dòng)態(tài)類型系統(tǒng)
量本身并不需要具有類型,因?yàn)榱坎恍枰P(guān)心其中的值是什么。但是很多語(yǔ)言中量同樣是具有類型的,而這種類型實(shí)際上是一種約束:被聲明為某個(gè)類型的量只能指向某個(gè)類型的值,如果這條規(guī)則被違反,那么編譯器被期望能夠產(chǎn)生編譯時(shí)錯(cuò)誤。
之所以需要這么做,是因?yàn)榇蠖鄶?shù)編譯型語(yǔ)言必須在編譯時(shí)確定值對(duì)應(yīng)的實(shí)現(xiàn),并且無(wú)法在運(yùn)行時(shí)確定值的類型。如果某個(gè)量被允許指向?qū)崿F(xiàn)不同的值,那么編譯器就必須用某種方式為其選擇實(shí)現(xiàn)。能夠在編譯時(shí)選擇實(shí)現(xiàn)的特性叫做編譯時(shí)多態(tài),著名的標(biāo)準(zhǔn)模板庫(kù)(STL)所使用的“模板”就是編譯時(shí)多態(tài)的一種實(shí)現(xiàn)。當(dāng)然也有運(yùn)行時(shí)多態(tài),C++的虛函數(shù)就是一種運(yùn)行時(shí)多態(tài)的實(shí)現(xiàn)。
不過(guò)雖然具有多態(tài)和運(yùn)行時(shí)類型標(biāo)注,C++的類型系統(tǒng)依然是靜態(tài)的。
與之相反的,JavaScript使用了動(dòng)態(tài)類型系統(tǒng),因而不對(duì)量能夠指向的值的類型做約束,相對(duì)的,JavaScript在運(yùn)行時(shí)進(jìn)行值類型檢查,如果值的類型不符,則會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤:事實(shí)上,JavaScript引擎從來(lái)不會(huì)放過(guò)任何一個(gè)運(yùn)行時(shí)類型錯(cuò)誤,畢竟對(duì)于這種錯(cuò)誤,引擎根本不知道如何執(zhí)行下去,所以就只好報(bào)錯(cuò)了:和某個(gè)著名web腳本語(yǔ)言相比,這反而是一件好事。
動(dòng)態(tài)類型系統(tǒng)更為靈活,但也犧牲了在編譯時(shí)提前進(jìn)行類型檢查的能力:這就使得某些編譯時(shí)錯(cuò)誤成了運(yùn)行時(shí)錯(cuò)誤。
不過(guò)畢竟JavaScript也并不存在編譯時(shí)。
厘清量與值的區(qū)別,我們就能更好得理解JavaScript以及其他的動(dòng)態(tài)類型語(yǔ)言背后的邏輯,希望這篇文章能夠給大家?guī)?lái)更多的一時(shí)爽和更少的火葬場(chǎng)。
===============================================
附加題:'1'+1='11'是誰(shuí)的錯(cuò)?
* 靜態(tài)類型語(yǔ)言是如何應(yīng)對(duì)這種情況的?
* 如果讓你來(lái)寫ECMA262,你會(huì)如何避免這種錯(cuò)誤?
* 某個(gè)著名web腳本語(yǔ)言沒有將operator+重載為字符串拼接運(yùn)算符,對(duì)此你怎么看?