服裝網(wǎng)站建設(shè)規(guī)劃方案佛山今日頭條
本文為《人人都能讀標(biāo)準(zhǔn)》—— ECMAScript篇的第12篇。我在這個(gè)倉(cāng)庫(kù)中系統(tǒng)地介紹了標(biāo)準(zhǔn)的閱讀規(guī)則以及使用方式,并深入剖析了標(biāo)準(zhǔn)對(duì)JavaScript核心原理的描述。
ECMAScript有7種原始類(lèi)型,分別是Undefined、Null、Boolean、String、Number、BigInt、Symbol。
本節(jié)中,我會(huì)先講這7種原始類(lèi)型的創(chuàng)建方式;然后我會(huì)談到從標(biāo)準(zhǔn)的角度看,在原始類(lèi)型上如對(duì)象一般調(diào)用方法是如何實(shí)現(xiàn)的;最后,我會(huì)對(duì)String和Number類(lèi)型的底層編碼形式進(jìn)行深入的講解。
原始類(lèi)型的創(chuàng)建
創(chuàng)建原始類(lèi)型的主要途徑是字面量。我們從字面量表達(dá)式的產(chǎn)生式可以看到,ECMAScript有4種原始類(lèi)型的字面量:
Null與Boolean
null字面量與Boolean字面量都非常簡(jiǎn)單,一個(gè)只能由終結(jié)符null
構(gòu)成,另一個(gè)只能由終結(jié)符true
或false
構(gòu)成。
Number與BigInt
數(shù)字字面量是我們?cè)谠砥脕?lái)解釋文法時(shí),舉的一個(gè)重要的例子。你還記得這張我們當(dāng)時(shí)對(duì)數(shù)字字面量文法的“解構(gòu)圖”嗎?
從解構(gòu)的結(jié)果我們可以看到,數(shù)字字面量不僅可以生成Number類(lèi)型,也可以生成BigInt類(lèi)型,總的來(lái)說(shuō),它有5種大的合法結(jié)構(gòu):
①十進(jìn)制數(shù)字:允許純數(shù)字:
100.5
、1
;也允許以小數(shù)點(diǎn)開(kāi)頭:.0005
;還允許使用指數(shù)e:100e-2
、.5e-3
;②十進(jìn)制bigInt:不允許有小數(shù)點(diǎn),也不允許使用指數(shù)e,且必須在數(shù)字后面添加
n
,如0n
、100n
;③非十進(jìn)制整數(shù),包括:
二進(jìn)制整數(shù):在二進(jìn)制數(shù)字(0和1)前面在
0b
或0B
:0b1010
、0B1010
;八進(jìn)制整數(shù):在八進(jìn)制數(shù)字(0~7)前面加
0o
或0O
:0o12
、0O12
;十六進(jìn)制整數(shù):在十六進(jìn)制數(shù)字(0~9與Aa~Ff)前面加
0x
或0X
:0x000A
、0X000a
;④非十進(jìn)制bigInt:與非十進(jìn)制整數(shù)一樣,只是后面需要多加一個(gè)
n
:0b1010n
、0o12n
、0x000An
;⑤老式的8進(jìn)制:在八進(jìn)制數(shù)字前面加0來(lái)表示8進(jìn)制:如
012
、00012
,現(xiàn)在這種寫(xiě)法已經(jīng)不被推薦了。
此外,對(duì)于這些不同的數(shù)字字面量具體會(huì)產(chǎn)生什么樣的數(shù)值,標(biāo)準(zhǔn)使用靜態(tài)語(yǔ)義NumericValue來(lái)表示他們的“取值”過(guò)程。
String
我們同樣可以像數(shù)字字面量一樣對(duì)字符串字面量進(jìn)行展開(kāi):
從展開(kāi)的結(jié)果看,字符串字面量包括雙引號(hào)字符串以及單引號(hào)字符串,且不包括字符串模版,字符串模版的文法在模版產(chǎn)生式中定義。
雙引號(hào)字符串與單引號(hào)字符串,除了引號(hào)不同,每個(gè)字符的構(gòu)成基本一樣,都是由5條代換式組成,以雙引號(hào)字符為例(圖中框出部分):
-
①:表示字符可以由除了雙引號(hào)
"
、行終結(jié)符、以及\
以外的所有Unicode字符構(gòu)成,這是我們最常使用的字符; -
②:表示行終結(jié)符中的
<LS>
可以直接作為字符串字符使用; -
③:表示行終結(jié)符中的
<PS>
可以直接作為字符串字符使用; -
④:表示那些通過(guò)使用
\
轉(zhuǎn)義后有特殊含義的字符或字符序列,包括:- 單轉(zhuǎn)義字符,包括
\b
、\t
、\n
、\v
、\f
、\r
、\"
、\'
、\\
。 - 八進(jìn)制轉(zhuǎn)義序列:使用
\
、\0
開(kāi)頭。 - 十六進(jìn)制轉(zhuǎn)義序列:使用
\x
開(kāi)頭,且后面只能跟兩個(gè)十六進(jìn)制數(shù)字。 - 碼點(diǎn)轉(zhuǎn)義序列:使用
\u
開(kāi)頭,有兩種寫(xiě)法,不帶{}
的寫(xiě)法必須跟4個(gè)十六進(jìn)制數(shù)字。
從對(duì)字符串取值的靜態(tài)語(yǔ)義SV我們可以得知:八進(jìn)制轉(zhuǎn)義序列、十六進(jìn)制轉(zhuǎn)義序列、碼點(diǎn)轉(zhuǎn)義序列最終都會(huì)根據(jù)其數(shù)字的值轉(zhuǎn)化為特定的碼點(diǎn),如下圖所示(如果你對(duì)碼點(diǎn)的概念不熟,后面的字符串編碼形式部分能夠幫到你):
基于這一點(diǎn),我們可以使用不同的字符串字面量表示同一個(gè)換行符號(hào)
\n
(碼點(diǎn)為10):"\n" === "\012"; // true - 八進(jìn)制轉(zhuǎn)義序列 "\n" === "\x0A"; // true - 十六進(jìn)制轉(zhuǎn)義 "\n" === "\u000A" // true - 碼點(diǎn)轉(zhuǎn)義序列(寫(xiě)法1) "\n" === "\u{A}" // true - 碼點(diǎn)轉(zhuǎn)義序列(寫(xiě)法2)
- 單轉(zhuǎn)義字符,包括
-
⑤:第五條代換式是對(duì)行終結(jié)符的轉(zhuǎn)義,這甚至使得你可以不借助字符串模版,直接在字符串中換行:
console.log("\我完全合法!")
undefined
undefined沒(méi)有字面量文法,因而無(wú)法通過(guò)字面量創(chuàng)建。當(dāng)我們?cè)诖a中如字面量一般地使用undefined
時(shí),實(shí)際上,它訪問(wèn)的是全局對(duì)象上的undefined屬性:
// undefined是全局對(duì)象上的屬性
undefined in window // true
// 全局對(duì)象上的undefined屬性不可修改
Object.getOwnPropertyDescriptor(window, "undefined") // {value: undefined, writable: false, enumerable: false, configurable: false}
undefined也不是保留字,所以你可以用undefined作為變量的標(biāo)識(shí)符:
{let undefined = 1 // 聲明一個(gè)名為undefined的變量let a // 未賦值的變量會(huì)初始化值為undefinedconsole.log(a === undefined) // falseconsole.log(a === void 0) // true
}
有的代碼,你會(huì)發(fā)現(xiàn)像這里一樣使用不太常見(jiàn)的void運(yùn)算符,void運(yùn)算符可以用來(lái)獲得“純正”的undefined,我們可以從它的求值語(yǔ)義中得到這個(gè)信息:
Symbol
Symbol也沒(méi)有字面量,它只能通過(guò)內(nèi)置對(duì)象Symbol創(chuàng)建:
Symbol("foo")
Symbol.for("foo")
標(biāo)準(zhǔn)中定義了一系列常用的Symbol,這些Symbol常常作為對(duì)象的插件使用。
在原始類(lèi)型上“調(diào)用”方法
我們對(duì)于原始類(lèi)型的方法調(diào)用并不陌生:
10.334524 .toFixed(2) // '10.33'
" test ".trim() // "test"
原始類(lèi)型之所以可以調(diào)用方法,與成員表達(dá)式MemberExpression的求值語(yǔ)義息息相關(guān)。如果你經(jīng)常讀標(biāo)準(zhǔn),你就會(huì)發(fā)現(xiàn)成員表達(dá)式是一個(gè)存在感非常高的表達(dá)式。
通過(guò)成員表達(dá)式的產(chǎn)生式,我們很容易發(fā)現(xiàn),a.b
的形式會(huì)被解析為成員表達(dá)式。
而對(duì)a.b
形式的成員表達(dá)式的求值也非常簡(jiǎn)單明了:先獲得.
左側(cè)表達(dá)式的值,然后通過(guò)抽象操作 EvaluatePropertyAccessWithIdentifierKey獲得這個(gè)值上對(duì)應(yīng)的屬性:
這里的“玄機(jī)”來(lái)自于第二步的抽象操作GetValue。不管第一步得到的是不是一個(gè)對(duì)象,GetValue會(huì)把第一步獲得的結(jié)果使用抽象操作ToObject轉(zhuǎn)化為對(duì)象。
于是,我們?cè)赥oObject中就能看到不同的數(shù)據(jù)類(lèi)型,轉(zhuǎn)換為對(duì)象的結(jié)果:
在這里,undefined與null無(wú)法進(jìn)行轉(zhuǎn)換,因?yàn)镋CMAScript沒(méi)有設(shè)計(jì)與這兩個(gè)類(lèi)型對(duì)應(yīng)的內(nèi)置對(duì)象,所以在undefined和null頭上使用“成員表達(dá)式”會(huì)報(bào)錯(cuò):
undefined[2]; // ?:Uncaught TypeError: Cannot read properties of undefined (reading '2')
null[2] // ?: Uncaught TypeError: Cannot read properties of null (reading '2')
其他的原始類(lèi)型都會(huì)轉(zhuǎn)化為各自的內(nèi)置對(duì)象,于是,就可以使用各自的內(nèi)置對(duì)象上的方法。
在轉(zhuǎn)化結(jié)果的描述中,不同的對(duì)象都有一個(gè)叫做“內(nèi)部插槽”的東西,使用[[]]
表示。關(guān)于內(nèi)部插槽,我會(huì)在13.對(duì)象類(lèi)型進(jìn)行解釋。
String類(lèi)型的編碼形式
String類(lèi)型表示程序中的字符串。而談到字符串,就離不開(kāi)字符集(Character set) 與字符編碼(Character Encoding) 。
現(xiàn)行世界中主要使用的字符集是Unicode,包含將近15萬(wàn)個(gè)字符。對(duì)于Unicode主要的兩種字符編碼形式分別是UTF-8以及UTF-16。其中HTML默認(rèn)使用的是UTF-8,而ECMAScript默認(rèn)使用的是UTF-16。
不管是使用UTF-8還是UTF-16,都可以表示Unicode中所有的字符。而這里有兩個(gè)重要的概念:
- 碼點(diǎn)(code point) :字符編碼中,每個(gè)Unicode字符對(duì)應(yīng)的數(shù)字映射。
- 碼元(code unit) :字符編碼中,碼點(diǎn)的最小組成單位。
在UTF-8中,一個(gè)碼元用一個(gè)字節(jié)(8位)表示,一個(gè)碼點(diǎn)用1到4個(gè)碼元表示;在UTF-16中,一個(gè)碼元用兩個(gè)字節(jié)(16位)表示,一個(gè)碼點(diǎn)用1個(gè)或2個(gè)碼元表示。
UTF-16的編碼模型
在講模型之前,有一個(gè)事情需要先搞清楚。在ECMAScript,數(shù)字類(lèi)型的十六進(jìn)制以0x
開(kāi)頭,如0x000A
;表示碼點(diǎn)的字符串的十六進(jìn)制以\x
或\u
開(kāi)頭,如"\x0A"
、"\u000A"
,如果你不弄清楚這一點(diǎn),就常常會(huì)被它們寫(xiě)法的切換弄得暈頭轉(zhuǎn)向。
正如前面所說(shuō),在UTF-16中,一個(gè)碼元使用兩個(gè)字節(jié)表示,因此每個(gè)碼元能夠表示的區(qū)間是[0x0000, 0xFFFF]。在這個(gè)區(qū)間內(nèi),UTF-16又劃分出一個(gè)高代理碼元(high-surrogate code unit),區(qū)間為[0xD800,0xDBFF],以及一個(gè)低代理碼元(low-surrogate code unit),區(qū)間為[0xDC00, 0xDFFF],如下圖所示:
在ECMAScript中,碼元是按照以下方式轉(zhuǎn)化為碼點(diǎn)的:
-
當(dāng)一個(gè)碼元即不是高代理碼元,也不是低代理碼元的時(shí)候,可以直接轉(zhuǎn)化為碼點(diǎn);
-
當(dāng)連續(xù)的兩個(gè)碼元c1、c2,前一個(gè)位于高代理碼元,后一個(gè)位于低代理碼元的時(shí)候,他們將構(gòu)成一個(gè)代理對(duì)(surrogate pair),并通過(guò)以下的公式計(jì)算出碼點(diǎn)的值:
(c1 - 0xD800) × 0x400 + (c2 - 0xDC00) + 0x10000
-
其他情況,碼元都會(huì)被直接轉(zhuǎn)化為碼點(diǎn)。
比如,下面的代碼將一個(gè)高代理碼元與一個(gè)低代理碼元組合,得到了新的碼點(diǎn):
console.log("\uD83D" + "\uDE0A')" // '😊'
UTF-16在實(shí)際代碼中的應(yīng)用
在ECMAScript中,字符串的length
方法計(jì)算的是字符串中碼元的數(shù)量,而不是碼點(diǎn)的數(shù)量:
"😊".length // 2
"😊"[0] // '\uD83D'
"😊"[1] // '\uDE0A'
在字符串方法的命名中,ECMAScript習(xí)慣使用charCode表示一個(gè)碼元,使用codePoint表示一個(gè)碼點(diǎn),相關(guān)的方法包括:
-
String.prototype.charCodeAt(pos):返回位置pos上碼元的值。
"😊".charCodeAt(0) // 55357, 16進(jìn)制表示是0xD83D "😊".charCodeAt(1) // 56842, 16進(jìn)制表示是0xDE0A
-
String.prototyoe.codePointAt(pos):返回位置pos上碼點(diǎn)的值。
"😊".codePointAt(0) // 128522 "😊".codePointAt(1) // 56842
-
String.fromCharCode(codeUnit):把碼元轉(zhuǎn)化為字符。
String.fromCharCode(128522) // ? String.fromCharCode(55357, 56842) // 😊
-
String.fromCodePoint(codePoint):把碼點(diǎn)轉(zhuǎn)化為字符。
String.fromCodePoint(128522) // 😊 String.fromCodePoint(55357, 56842) // 😊
在這里,我們甚至還可以使用這些結(jié)果驗(yàn)證前面兩個(gè)碼元拼成一個(gè)碼點(diǎn)的公式:
(c1 - 0xD800) × 0x400 + (c2 - 0xDC00) + 0x10000
因?yàn)槲覀円呀?jīng)知道,“😊”是由兩個(gè)碼元組成,數(shù)值分別為55357與56842,于是,我們可以在String.fromCodePoint
中應(yīng)用這條公式:
String.fromCodePoint((55357 - 0xD800) * 0x400 + (56842 - 0xDC00) + 0x10000) // 😊
Number類(lèi)型的編碼形式
從上面我們可以看出,String類(lèi)型中的每個(gè)字符實(shí)際上是由內(nèi)存中16位或32位二進(jìn)制表示的。而Number類(lèi)型則是使用64位二進(jìn)制表示,具體采用的是《IEEE 754-2019》定義的雙精度浮點(diǎn)數(shù)格式,在其他編程語(yǔ)言中,這種浮點(diǎn)數(shù)類(lèi)型也常用float64表示。
雙精度浮點(diǎn)數(shù)模型
IEEE 754把64位分成3個(gè)部分:
- 符號(hào)位(sign):占用1位
- 指數(shù)位(exponent):占用11位
- 有效數(shù)位(fraction):占用52個(gè)位
懶得做圖了,這里直接使用維基百科提供的圖:
在這個(gè)模型中,任意數(shù)字都可以使用以下的公式表示:
在這條公式中,sign表示符號(hào)位,b表示有效數(shù)位,e表示指數(shù)位。
這里需要注意的是:這是一條混合了十進(jìn)制和二進(jìn)制數(shù)字的計(jì)算公式。 中間部分的(1.b51b52...b0)
是二進(jìn)制表示,一個(gè)帶有小數(shù)點(diǎn)的二進(jìn)制。后面的2^e-1023
是十進(jìn)制數(shù)字,不搞清楚這一點(diǎn),你就無(wú)法得到正確的結(jié)果。
比如,數(shù)字1.5,可以用以下內(nèi)存空間表示(這張圖是我在一個(gè)float64二進(jìn)制轉(zhuǎn)換器中獲得的,這里的Mantissa(尾數(shù))相當(dāng)于上面的fraction(有效數(shù)位)):
此時(shí):
- 符號(hào)位為0,于是符號(hào)部分的計(jì)算相當(dāng)于
-1^0
,表示正數(shù); - 指數(shù)位為
01111111111
,轉(zhuǎn)為十進(jìn)制后是1023。于是,指數(shù)部分計(jì)算為2^(1023 - 1023)
,結(jié)果為1; - 在有效數(shù)位上,我們得到的是一個(gè)
1.1
的二進(jìn)制小數(shù),轉(zhuǎn)化為十進(jìn)制的方式以及結(jié)果是2^0 + 2^-1 = 1 + 0.5 = 1.5
所以,最終的計(jì)算結(jié)果為:
(-1^0) * (1.5) * 2^0 = 1.5
按理說(shuō),64位二進(jìn)制應(yīng)該可以表示2^64個(gè)數(shù)字。但實(shí)際上,ECMAScript的數(shù)字只有2^64 - 2^53 + 3
種,這是因?yàn)?#xff1a;當(dāng)一個(gè)數(shù)字的指數(shù)位全是1的時(shí)候,被設(shè)計(jì)為不能通過(guò)以上的公式轉(zhuǎn)化為實(shí)際的數(shù)字,這類(lèi)特殊的數(shù)字有2^53
個(gè)(1個(gè)符號(hào)位 + 52個(gè)有效數(shù)位)。在ECMAScript中,這類(lèi)特殊的數(shù)字會(huì)被轉(zhuǎn)化為另外3種特殊的值:
-
如果此時(shí)有效數(shù)位全為0,符號(hào)位也是為0,在ECMAScript表示為+Infinity;
-
如果此時(shí)有效數(shù)位全為0,符號(hào)位為1,則表示為-Infinity;
-
除此以外,其他的值都表示為NaN(Not a Number)。
精度丟失問(wèn)題
使用浮點(diǎn)數(shù)表示數(shù)字的優(yōu)勢(shì)在于,它能夠表示的數(shù)字范圍比整點(diǎn)數(shù)更廣。但它的缺點(diǎn)在于,有時(shí)候會(huì)有精度丟失的問(wèn)題。稍有經(jīng)驗(yàn)的前端都明白,在JS中,0.1 + 0.2 不精準(zhǔn)等于 0.3,核心原因在于:雙精度浮點(diǎn)數(shù)模型根本無(wú)法精確表示0.1、0.2、0.3。
你完全可以在上面我提供的轉(zhuǎn)換器中測(cè)試一下:把0.3轉(zhuǎn)化成二進(jìn)制,再把對(duì)應(yīng)的二進(jìn)制反向轉(zhuǎn)化一下,得到的只是一個(gè)無(wú)限接近0.3的數(shù)字,而不是0.3本身。