你不知道的 JavaScript 基礎類型
前言
今天聊下 js 中的數據類型,數據類型是一個語言的基石,那你真的瞭解我們工作中這些常使用的數據類型嗎。可以先看以下幾個問題?看看你能直接回答上來幾個。本文將從這些問題入手,主要講解我們在使用中容易模糊和產生歧義的點,加深我們對數據類型的理解。
問題
-
爲什麼說
undefined
是變量?那我們能改變它嗎? -
爲什麼
typeof null 是 'object'
?null 是對象嗎? -
0.1+0.2 爲什麼不等於 0.3?
-
爲什麼
let s = '𠮷'; s.length === 2
? -
爲什麼
"𠮷".charAt(0) // '\uD842'
?
概述
在 JavaScript 中的類型系統可以分爲以下七類,我們稱之爲語言類型,js 不支持自己定義類型,所以在前端代碼中所有值都來源於以下七種類型:
-
Undefined
-
Null
-
Boolean
-
Number
-
String
-
Symbol
-
Object
前六個是簡單數據類型(原始值),Object
是複雜數據類型,也是 js 中的大 boss,整個 js 語言都是在對象的基礎上建立的,依靠 js 異常靈活的類型系統,現有的 7 種類型可滿足幾乎所有使用場景。
Undefined
Undefined
類型的值只有一個就是'undefined'
,屬於 js 中的特殊類型,自身的含義表示一個未定義的值,這個值在ES3
版本以前是不存在的,引入的目的是爲了明確空對象指針 (null) 與未初始化變量的區別。
應用場景:當聲明一個變量並沒有賦值時,就相當於給變量賦值了一個undefined
,所以在任何情況下,我們都沒必要給一個變量顯式的賦值爲 undefined,所以undefind
並沒有實際的主動使用的場景。
爲什麼說
undefined
是變量?那我們能改寫它嗎?
在 MDN 上對此的描述大概是:它掛載在全局對象,是全局作用域下的一個屬性,這個屬性的最初值就是原始數據類型undefined
。
那問題又來了,既然是變量那我們能改變它嗎? 這個問題真是毫無意義,我們在任何時候都不會,不應該,不可能去嘗試改變undefined
的值。
而實際上現代瀏覽器自 ES5 標準以來undefined
就是一個不能被配置(non-configurable),不能被重寫(non-writable)的屬性。** 可是在局部作用域中我們仍然可以聲明一個名爲undefined
的變量,去覆蓋全局作用域下的undefined
**,因爲在 ES 中,undefined
既不是關鍵字也不是保留字(無聊的知識增加了),請看下圖:
爲了符合編程規範,有些文章會提到可以使用void
操作符合理合法的去獲取undefined
的值,。。。寫起來還蠻耳目一新的(無聊的知識又增加了)。
總之,無論這是設計失誤還是有意爲之,對我們對於該語言的使用基本沒有什麼影響,簡單瞭解下就好,這個其實也屬於是沒什麼價值和意義的問題。
Null
Null
類型的值只有一個就是'null'
,與 undefined 類似,也屬於 js 中的特殊類型,既然null
是一個原始類型,那就有了這個問題了:
爲什麼
typeof null 是 'object'
?null 是對象嗎?
這結果就很奇葩,但仍然表現如此,那多數就是歷史原因導致,所以我們無需糾結null
到底是原始值還是一個對象。《JavaScript 高程 4》中對此有比較合乎邏輯的定義:null 值表示一個空對象的指針,null
當然是個原始類型的值,但它也是個空對象的指針,這也解釋了爲什麼typeof null === 'object'
。
由此我們也應該能理解,null
雖然含義與undefined
類似都表示空,但null
表示的是一個空對象,當我們要聲明一個變量準備賦值一個對象,卻在當時沒有一個具體的對象可保存時,就要使用 null 來填充該變量,我們永遠不會主動的去賦值一個undefined
,卻經常會主動賦值一個null
表示一個對象的初始值。
值得一提的是,當我們去判斷undefined == null
時,會返回一個true
,又是一個迷惑性的操作,背後原因是==
操作符讓值做了隱式類型轉換,這也是 js 類型系統異常靈活的原因。
Boolean
布爾值,值有兩個true
和false
Number
Number
類型最常用來表示我們常規意義上的十進制數字,也能使用八或十六進制,除此之外還有一些特殊的值如NaN、Infinity、-Infinity
等,相關知識點雖多但大多比較容易理解。這裏要專門聊的是 js 老生常談的Number
浮點值精度不足的問題:
0.1+0.2 爲什麼不等於 0.3?
前端:王德發??
ES:雨我無瓜。
Number
浮點值表現出來的這種特性來源於 ES 採用的 IEEE754 二進位浮點數算術標準,該標準運用廣泛,很多常見的編程語言(如 C++、C#、Java)都使用該標準來處理數據的存儲與計算,實際上任何採用此標準的語言,都會有以上特性(有些語言通過內部封裝幫助解決該問題)。而 ES 正是採用其中的雙精度浮點數規則。
雙精度浮點數是計算機中常見的一種數據格式,在內存中佔 64 位。計算機對 Number 類型做存儲時,需將其轉化爲二進制做存儲,十進制小數轉爲二進制時,會出現二進制位數超出處理範圍的問題,如 0.1(0.000110011... 0011 死循環), 計算機會通過 0 舍 1 入來存儲處理範圍內的位數,此時誤差就出現了,但是由於保留位數很多,誤差將非常小,可忽略不計。
但當我們需要測試某個特定的浮點值時,可能就會產生錯誤,所以我們在程序中儘量不去驗證某個特定的浮點值。ES6 之後新增了Number.EPSILON
屬性,表示數字最小間隔,也可用它來比較判斷,是正常誤差值還是個錯誤。
(0.1+0.2)-0.3<=Number.EPSILON //true
ES 也爲我們處理了其他一些場景,如 1/0 並不會拋出錯誤,而是定義了 Infinity 的無窮值,非數值會表示爲特殊的 NaN 值等。
String
本節探討下字符串在 ES 中是如何做存儲的,字符串有個非常常用的length
屬性,表示字符數量,憑藉我們程序員最樸素的情感,通常認爲length
的值會與我們眼睛看到的結果是一致的,但偶爾會突然發現一些不一樣的情況,如下:
let s = '𠮷'; console.log(s.length) // 2
栓 Q,又被騙到
實際上字符串的存儲要比我們看到的複雜的多,如何將字符串真正存儲到計算機中,這裏涉及到兩個多數人都聽過,但可能又不是特別瞭解的概念,字符集和字符編碼。相關知識點很多可以單開一篇文章,下面簡單講解下。
字符集
字符集相當於一個密碼本,在一個字符集中每個字符會對應一個固定的編號(碼點),編號可以使用數字代替,而字符則可能是各種各樣的文字表情、字母符號、圖形圖像等一切人們使用的符號。如果我們把全世界所有文字都放到一個字符集中,那就在計算機中實現了世界文字的統一。
而現在也正有這樣的一個字符集那就是大名鼎鼎Unicode
,這個字符集囊括了迄今爲止世界上所有的文字,到今天發佈到了 15.0 版本,收錄了 149,186 個文字,已經成爲計算機中使用最廣泛的字符集標準。
Unicode
使用數字給字符做唯一編號,通常使用十六進制表示,會在U+0000~U+10FFFF
範圍定義字符,能使用的總數大概是一百多萬個,目前只有十分之一被定義了字符。比如U+597D
代表中文字好。
Unicode
將字符集範圍分爲了 17 個平面,前面的 65536 個字符位,稱爲基本多語言平面(BMP),它的碼點範圍就是 U+0000~U+FFFF。所有最常見的字符都放在這個平面,是 Unicode 最先定義的一個平面,其他字符放在其他 16 個平面,稱爲增補平面(SMP),
字符編碼
而字符編碼是指計算機要如何將Unicode
中的字符編號存入計算機中,是一種編碼方式,每個字符集都有其對應的編碼方式,而Unicode
對應的編碼方式就是我們常聽到的,UTF8、UTF16、UTF32。特點如下:
-
UTF8:1-4 字節,變長
-
UTF16:2 或 4 字節,變長
-
UTF32:4 字節,定長
編碼方式
我們知道計算機只能存儲二進制數,所以當我們知道一個字符的十六進制碼點數(字符編號),只要把它轉成二進制,存到計算機中即可,而編碼就是如何轉換和存儲的過程。
雖然以上三種編碼方式目的都是將碼點轉成二進制數,但轉換的方式、轉換後存儲的二進制數、計算機讀取二進制的方法都是不同的,這是因爲在 U+0000~U+10FFFF 範圍的碼點,轉換成二進制存儲,最小的只需要 1 個字節,最多需要 3 個字節
-
如果我們採用 UTF32 編碼方式,分配給每個字符的內存都將是固定的 4 個字節 / 32 位,這是最直觀的存儲方式,
Unicode
中所有碼點轉成的二進制數都是 32 位以內,所以計算機只需要直接將碼點轉成二進制存入計算機即可,讀取時計算機固定按 4 個字節爲一個字符的規則去讀取。簡單明瞭,但該方法將造成大量的空間浪費,比如排在前面的 ASCII 碼的字符,只需要 1 個字節 / 8 位去存儲,將浪費 3 個字節。這種編碼方式很少使用,互聯網中普遍採用 UTF8 變長的方式去編碼。 -
如果我們採用 UTF16 編碼方式,分配給每個字符的內存將會分爲 2 或 4 字節的定長,對於 U+0000~U+FFFF 範圍的基本平面碼點,使用 2 字節 / 16 位直接轉換存儲。對於 U+010000~U+10FFFF 輔助平面碼點,轉換成二進制將大於 16 位,UTF16 會將其拆成兩半,使用兩個 2 字節 / 16 位去存儲,該方法稱爲代理對。如𠮷(0xD842,0xDFB7)
UTF-16 將這 20 位拆成兩半,前 10 位映射在 U+D800 到 U+DBFF(空間大小 210),稱爲高位(H),後 10 位映射在 U+DC00 到 U+DFFF(空間大小 210),稱爲低位(L)。這意味着,一個輔助平面的字符,被拆成兩個基本平面的字符表示。
- UTF8 具體編碼方式,可參考此文 ASCII,Unicode 和 UTF-8
JS 中的字符編碼
說了這麼多,我們再聊回 js,js 中到底採用哪種編碼方式是 UTF8 還是 UTF16 呢,重點來了,都不是!js 採用的是 UTF16 和 UCS-2 的編碼混合策略,從今天看來 UTF16 可以算是 UCS-2 的升級版。爲什麼 js 不直接採用 UTF16 呢,因爲 js 首次面世時 UTF16 還未推出,兩者混用也算是一個歷史遺留問題。
對於 U+0000~U+FFFF 基本平面的字符 UCS-2 與 UTF16 是完全沒區別的,我們日常使用的絕大部分字符都來源於這個平面,所以 js 開發者在一般情況下對此無感。
而 UCS-2 是固定的將 2 字節 / 16 位認爲是一個字符(因爲 Unicode 早期只有一個平面,16 位已經完全足夠,後期進行了擴容)。當字符中出現基本平面之外的字符,因爲上文說的代理對策略,該字符會用兩個 2 字節 / 16 位去存儲,而 UCS-2 固定的認爲 2 字節就是一個單獨的字符,所以此時使用字符類的操作時,會出現錯誤,所以纔會出現上述問題'𠮷'.length = 2
當然這些都算是歷史問題,自 ES6 推出,Unicode
相關的編碼問題已經得到解決,ES 也完全有能力自動識別字符是 2 個字節還是 4 個字節,但開發者對於.length
的使用習慣由來已久,爲了保證兼容性,並未對其結果做出修正。
結尾
由於衆所周知的歷史原因和複雜多樣的執行環境,JavaScript 在使用過程中可能會遇到各種奇怪難理解的現象,有一些是因爲更深層的底層原理,也有很多隻是因爲設計失誤或歷史包袱,在這裏作者建議我們在學習中只聚焦少部分有價值的問題,而忽略無意義的探究。
參考文獻
-
《JavaScript 高級程序設計 4》
-
IEEE 754 雙精度浮點數
-
字符集與編碼(二)——編號 vs 編碼
-
Unicode 與 JavaScript 詳解
-
ASCII,Unicode 和 UTF-8
-
The Absolute Minimum Every Software Developer Absolutely...
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wtyMVQzzCnuIDeYpqfDoaA