【JavaScript】類型系統的細節

在前端技術飛速發展的今天,彷佛全部人都沉浸在前端框架的學習上。在這些框架和新技術推出以後,好像一晚上之間就會有《深刻解讀xx框架》、《手把手教你寫一個xx》這樣的文章,看得人是眼花繚亂,同時我也在想,爲何有些人不是開發團隊的人,可是能夠這麼快就寫出對框架解讀的文章?是做弊了嗎?顯然技術的道路上沒有捷徑,借用以前在TFC(騰訊前端大會)上聽到的React-China的站長說的一句話:「其實我就是學東西快。」然而這句話也不能光從字面意思去理解,大佬之因此學東西快,除了智商碾壓的部分,實際上是由於大佬的基礎知識很牢固。javascript

因此從這篇文章開始,我會根據本身的理解和學習狀況,總結JavaScript中一系列常見的重點的知識點(但不會是簡單地羅列概念),但願以此創建一個系統的前端知識體系。html

而今天就從JavaScript的類型系統開始。前端

有哪些數據類型

咱們已經知道,在JavaScript中有七種類型:java

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Symbol
  • Object

前六種是原始類型(primitive),而Object(對象)是引用類型。除此以外,其它全部的類型都是從Object繼承的。若是想驗證一下的話,能夠在控制檯聲明一個引用類型的變量,而後輸出這個變量,能夠看到,原型鏈的最末端都是Objectsegmentfault

能夠看到,雖然arr是一個數組類型的變量,可是__proto__仍然指向了Object。數組

typeof操做符

typeof是咱們很熟悉的一種簡單檢測變量數據類型的操做符。用typeof操做簡單類型時,除了Null類型,其它類型都能正確返回對應的類型;對Object和Null類型,都會返回object;而對於函數,則會返回function。瀏覽器

對於typeof不能正確識別Null、引用類型和自定義類型,在我看過的文章裏,無不說是設計錯誤,是JavaScript的歷史包袱。沒錯,不能正確識別Null是由於:安全

在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示類型的標籤和實際數據值表示的。對象的類型標籤是 0。因爲 null表明的是空指針(大多數平臺下值爲 0x00),所以,null 的類型標籤是 0,typeof null 也所以返回 "object"。-- From MDN前端框架

可是其不能正確識別引用類型咱們是否是能夠這樣理解:typeof操做符返回的是變量原型鏈最頂端原型的類型。框架

固然,在JavaScript的世界裏,萬物皆對象,若是按照上面的理解,對函數使用typeof操做符應該也返回object,然鵝函數也確實有一些特殊的屬性,所以經過typeof來區分函數和其餘對象是有必要的 -- From 《JavaScript高級程序設計》

Undefined

Undefined類型只有一個值 -- undefined。在對變量聲明(var或let)但未初始化時,這個變量的值是undefined:

var name // undefined
let age // undefined
const gender // Uncaught SyntaxError: Missing initializer in const declaration

若是咱們不顯式地初始化變量,即初始化時就給變量賦值,那麼這個時候就會出現一個矛盾:

var name
console.log(typeof name) // undefined
​
// age沒有聲明
// var age
console.log(typeof age) // undefined

此時就很難區分變量究竟是聲明瞭未賦值仍是根本沒有聲明。所以,從代碼規範的角度來看,顯式地初始化變量仍是頗有必要的。這樣當typeof返回undefined時,就能清楚地知道是變量尚未被聲明,而不是聲明瞭未賦值。

關於undefined還有一個小問題須要注意 -- 由於undefined不是關鍵字,因此undefined是能夠被重寫的。咱們能夠經過如下的代碼測試

var undefined = 'overwrite undefined'
console.log(undefined)

好吧,我認可把你帶偏了,若是直接在控制檯輸入這段代碼,輸出的還是undefined,緣由是ECMAScript 5中修復了在window下重寫undefined的問題,咱們能夠經過如下代碼查看window下的undefined的屬性

Object.getOwnPropertyDescriptor(window, 'undefined')
​
// { value: undefined, configurable: false, writable: false, enumerable: false }

可是不能高興得太早,咱們來看這個例子

(function () {
  var undefined = 'overwrite undefined!'
​
  console.log(undefined)
  console.log(undefined === window.undefined)
})()

這段代碼會分別輸出overwrite undefined和false,因此對於以前說的結論應該作一點修改 -- 因爲undefined不是關鍵字,在函數做用域下是能夠被重寫的,可是window下是不可被重寫的

那當咱們使用到undefined的時候,若是避免相似的狀況發生呢?其實很簡單,使用void 0代替便可。

void是JavaScript中的一個操做符,語法是void 表達式,做用是計算後面的表達式並返回undefined,這是一個比直接用undefined更安全的作法。

Null

Null類型只有一個值,就是null,表示定義了但爲空。最開始說到,null表示的是空指針,因此若是定義了變量是準備用來保存對象的話,最好將這個變量初始化爲null。這樣作不只能夠體現null做爲空對象指針的習慣,方便檢查變量是否已經有對象引用,同時也更方便地區分null和undefined。

另外,null在JavaScript中是一個關鍵字,因此不用擔憂其會被重寫。

Boolean

Boolean類型表示的是邏輯真假值,這個類型有兩個值 -- true和false,且爲關鍵字。

對於這兩個值其實沒有太多須要注意的,不過其它任意的類型的值均可以轉換成對應的邏輯值

這些轉換規則對於各類流程控制語句中的判斷條件是很是重要的。

Number

在JavaScript中,有2^64 - 2^53 + 3個值,由於NaN佔用了9007199254740990,還有-Infinity和Infinity。

同時,JavaScript中能夠保存+0和-0,它們倆在值的比較上是相等的,在作加法類的運算時也是相等的,可是在除法的場合就不同了。當它們分別作分母時,獲得的值分別是Infinity和-Infinity。所以,若是想區分+0和-0,最好的方式是檢查1/x是Infinity仍是-Infinity。

在JavaScript中有效的整數範圍是-0xffffffffffffffff 到 0xffffffffffffffff,在這個範圍以外的數字沒法精確表示。

而最經典的0.1 + 0.2 == 0.3會返回false 的問題,根據浮點數的定義,0.1和0.2的二進制表示分別爲

Number(0.1).toString(2) // "0.0001100110011001100110011001100110011001100110011001101"
Number(0.2).toString(2) // "0.001100110011001100110011001100110011001100110011001101"

所以將二進制下的0.1和0.2相加以後,顯然就不等於0.3了。而解決精度丟失的方案有不少,最簡潔的實際上是比較等號左右的差值的絕對值是否是小於最小精度,即Number.EPSILON

console.log(Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON)

String

字符串也是平時開發用得不少的一種類型,在JavaScript中,字符串是由16位Unicode字符組成的字符序列,它有最大長度2^53-1,正好是JavaScript中能表示的最大安全整數

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1 // true

這個值大約有9PB,這個數字是很是大的了,不可能有一個變量要存這麼大的數據量,更況且,普通計算機也沒有這麼大的內存,有也不可能分配這麼大的內存給瀏覽器。

JavaScript字符串把每一個UTF16單元當作一個字符來處理,因此處理非BMP的字符(超出U+0000 - U+FFFF)時應格外當心。

Symbol

Symbol是ES6新增的基本類型,是一切非字符串的對象key的集合,並且ES6規範中的對象系統徹底用Symbol重塑了。

Symbol具備字符串描述,可是即便描述相同,Symbol也不相等

let s1 = Symbol('symbol')
let s2 = Symbol('symbol')
​
console.log(s1 == s2) // false

幾個須要注意的點:

  • 使用new運算符的語法會報TypeError
  • Symbol在for...in迭代中不可枚舉。Object.getOwnPropertyNames()不會返回Symbol對象爲key的屬性,可是能夠經過Object.getOwnPropertySymbols()獲取
  • 使用JSON.stringify()時,會忽略Symbol值做爲key的屬性
  • 當使用Symbol值做爲key時,這個key會被Object再包裝一次
var a = {}
var k = Symbol('a')
​
a[k] = 1
console.log(a[k]) // 1
console.log(a[Object(k)]) // 1

Object

在JavaScript的世界中,全部內容都是對象。關於對象的基礎知識就再也不贅述,這裏解答一個問題:爲何給對象添加的方法能用在基本類型上?

Number.prototype.test = () => console.log('test')
​
var num = 1
​
num.test() // test

首先,幾個基本類型在對象類型中都有對應的「映射」:

  • Number
  • String
  • Boolean
  • Symbol

前三種可使用new操做符,返回一個對應類型的實例。雖然對Symbol函數使用new操做符會報錯,但Symbol函數仍然是一個構造器。這四種基本類型在被轉換成對應的對象類型後,就能夠經過原型鏈訪問到對應的類型的原型。而上述代碼的最後一行中,. 操做符其實就是會臨時對num進行一次裝箱操做,將其轉換成Number對象類型,這樣就使得咱們看上去能在基礎類型上調用對應對象的方法。

類型轉換

衆所周知,JavaScript做爲動態語言,最大的特色之一就是變量的類型是能夠任意轉換的,並且咱們熟悉的運算幾乎都會先進行類型轉換。

圖片來自winter的重學前端

上圖中大部分的轉換是很符合咱們的直覺的,比較複雜的Number和String之間的轉換,以及對象和基本類型之間的轉換。

StringToNumber

將字符串轉換成數字有三個函數:

  • Number
  • parseInt
  • parseFloat

Number接受一個參數,而且按照必定的規則判斷參數是否能夠被轉換成數字。

圖片截圖自《JavaScript高級程序設計》

從圖中就能看出Number函數在內部作了不少判斷和操做。

parseInt接受兩個參數,第一個參數是要轉換的內容,第二個參數是轉換的進制。在不傳入第二個參數的狀況下,parseInt只支持16進制前綴‘0x’,並且會忽略第一個非16進制字符及其後的內容;若是是0開頭的數字,會由於不一樣的瀏覽器而使用不一樣的進制,所以全部狀況下使用parseInt都應該指定進制。

這兩個函數共同的問題都是不支持科學計數法。

有趣的是,Nicholas Zakas在《JavaScript高級程序設計》中說由於Number的判斷條件的複雜性,所以在處理整數時更經常使用的是parseInt。可是winter在重學前端中給出的建議是,多數狀況下,Number是比parseInt和parseFloat更好的選擇。

那對於普通開發者而言,應該使用Number仍是parseInt&parseFloat呢?我仍是以爲一切不基於業務場景和開發者自身狀況的技術選型都是耍流氓

BTW,若是平時只處理

  • 二進制、八進制、十六進制到十進制的轉換
  • 字符串數字到數字的轉換
  • 科學計數法到數字的轉換

可使用加號操做符進行類型的隱式轉換

var a = '99' // 字符串數字
var b = '0b111' // 二進制
var c = '0o123' // 八進制
var d = '0xFF' // 十六進制
var e = '1e3' // 科學計數法
​
console.log(+a) // 99
console.log(+b) // 7
console.log(+c) // 83
console.log(+d) // 255
console.log(+e) // 1000

裝箱轉換

裝箱轉換,即,將每一中基本類型轉換爲對應的對象。

雖然Symbol函數沒法用new來調用,可是能夠用Object函數顯式調用裝箱轉換

var symbolObject = Object(Symbol("a"));
​
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

全部被裝箱的對象都有私有的Class屬性,這個屬性的值能夠經過Object.prototype.toString獲取

var a = Object(Symbol('a'))
​
console.log(Object.prototype.toString.call(a)) // [object Symbol]

在JavaScript中,開發者是不能更改這個私有的Class屬性的,所以Object.prototype.toString能夠更加準確識別對象對應的基本類型,它比instanceof更準確。

拆箱轉換

在JavaScript中,規定了ToPrimitive函數,它是對象類型到基本類型的轉換,即,拆箱轉換。

拆箱轉換會嘗試調用valueOf和toString來獲取拆箱後的基本類型。若是二者都不存在,或者都沒有返回基本類型,則會產生類型錯誤TypeError。

對象到數字的轉換優先調用valueOf,再調用toString

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}
​
o * 2
// valueOf
// toString
// TypeError

而對象到字符串的轉換優先調用toString,再調用valueOf

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}
​
String(o)
// toString
// valueOf
// TypeError

而在ES6以後,還容許對象經過顯式指定toPrimitive Symbol來覆蓋原有的行爲

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}
​
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
​
console.log(o + "")
// toPrimitive
// hello

小結

文章裏講到的一些內容並無更加深刻地探討,主要是由於對應的主題會在以後的文章中再展開,並且一個主題也會花費大量的時間從新讀官方文檔和其它文章,因此今天的這個主題也算是一次試水吧。

本文的參考資料主要是:

後記

在通過從wordpress,到Express+Mongo,再到Koa+MySQL+Vue的博客搭建,我花費了大量的時間在簡單的CRUD和網站的樣式上,從而忽略了文章的細節。在年初把博客服務關閉以後,一直在想到底應該再以哪一種形式對本身的學習作好輸出呢?最終仍是迴歸了公衆號(半年前手殘把以前能夠留言的公衆號註銷了、Github和知乎。

以前的幾篇"文章"內容比較水,沒什麼技術含量,主要是爲了先發幾篇湊個數,從這一篇開始,我會盡可能作到周更,同時保證內容再也不太水,畢竟看文章的都是大佬,而能看到這裏可能也是爲了支持一下文章的完整閱讀率吧。

歡迎關注公衆號:Debugger

相關文章
相關標籤/搜索