作為 JavaScript 的入門級知識點,JS 資料型別在整個 JavaScript 的學習過程中其實尤為重要。因為在 JavaScript 程式設計中,我們經常會遇到邊界資料型別條件判斷問題,很多程式碼只有在某種特定的資料型別下,才能可靠地執行。
尤其在大廠面試中,經常需要你現場手寫程式碼,因此你很有必要提前考慮好資料型別的邊界判斷問題,並在你的 JavaScript 邏輯編寫前進行前置判斷,這樣才能讓面試官看到你嚴謹的程式設計邏輯和深入思考的能力,面試才可以加分。
因此,這一講我將從資料型別的概念、檢測方法、轉換方法幾個方面,幫你梳理和深入學習 JavaScript 的資料型別的知識點。
我希望透過本講的學習,你能夠熟練掌握資料型別的判斷以及轉換等相關知識點,並且在遇到資料型別判斷以及資料型別的隱式轉換等問題時可以輕鬆應對。
資料型別概念
JavaScript 的資料型別有下圖所示的 8 種:
其中,前 7 種類型為基礎型別,最後 1 種(Object)為引用型別,也是你需要重點關注的,因為它在日常工作中是使用得最頻繁,也是需要關注最多技術細節的資料型別。
而引用資料型別(Object)又分為圖上這幾種常見的型別:Array - 陣列物件、RegExp - 正則物件、Date - 日期物件、Math - 數學函式、Function - 函式物件。
在這裡,我想先請你重點了解下面兩點,因為各種 JavaScript 的資料型別最後都會在初始化之後放在不同的記憶體中,因此上面的資料型別大致可以分成兩類來進行儲存:
基礎型別儲存在棧記憶體,被引用或複製時,會建立一個完全相等的變數;
引用型別儲存在堆記憶體,儲存的是地址,多個引用指向同一個地址,這裡會涉及一個“共享”的概念。
關於引用型別下面直接透過兩段程式碼來講解,讓你深入理解一下核心“共享”的概念。
題目一:初出茅廬
let a = { name: 'lee', age: 18}let b = a;console.log(a.name); //第一個consoleb.name = 'son';console.log(a.name); //第二個consoleconsole.log(b.name); //第三個console
這道題比較簡單,我們可以看到第一個 console 打出來 name 是 'lee',這應該沒什麼疑問;但是在執行了 b.name='son' 之後,結果你會發現 a 和 b 的屬性 name 都是 'son',第二個和第三個列印結果是一樣的,這裡就體現了引用型別的“共享”的特性,即這兩個值都存在同一塊記憶體中共享,一個發生了改變,另外一個也隨之跟著變化。
你可以直接在 Chrome 控制檯敲一遍,深入理解一下這部分概念。下面我們再看一段程式碼,它是比題目一稍複雜一些的物件屬性變化問題。
題目二:漸入佳境
let a = { name: 'Julia', age: 20}function change(o) { o.age = 24; o = { name: 'Kath', age: 30 } return o;}let b = change(a); // 注意這裡沒有new,後面new相關會有專門文章講解console.log(b.age); // 第一個consoleconsole.log(a.age); // 第二個console
這道題涉及了 function,你透過上述程式碼可以看到第一個 console 的結果是 30,b 最後列印結果是 {name: "Kath", age: 30};第二個 console 的返回結果是 24,而 a 最後的列印結果是 {name: "Julia", age: 24}。
是不是和你預想的有些區別?你要注意的是,這裡的 function 和 return 帶來了不一樣的東西。
原因在於:函式傳參進來的 o,傳遞的是物件在堆中的記憶體地址值,透過呼叫 o.age = 24(第 7 行程式碼)確實改變了 a 物件的 age 屬性;但是第 12 行程式碼的 return 卻又把 o 變成了另一個記憶體地址,將 {name: "Kath", age: 30} 存入其中,最後返回 b 的值就變成了 {name: "Kath", age: 30}。而如果把第 12 行去掉,那麼 b 就會返回 undefined。這裡你可以再仔細琢磨一下。
講完資料型別的基本概念,我們繼續看下一部分,如何對資料型別進行檢測,這也是比較重要的問題。
資料型別檢測
資料型別檢測也是面試過程中經常會遇到的問題,比如:如何判斷是否為陣列?讓你寫一段程式碼吧 JavaScript 的各種資料型別判斷出來,等等。類似的題目會很多,而且在平常寫程式碼過程中我們也會經常用到。
我也經常在面試一些候選人的時候,有些回答比如“用 typeof 來判斷”,然後就沒有其他答案了,但這樣的回答是不能令面試官滿意的,因為他要考察你對 JS 的資料型別理解的深度,所以我們先要做到的是對各種資料型別的判斷方法瞭然於胸,然後再進行歸納總結,給面試官一個滿意的答案。
資料型別的判斷方法其實有很多種,比如 typeof 和 instanceof,下面我來重點介紹三種在工作中經常會遇到的資料型別檢測方法。
第一種判斷方法:typeof
這是比較常用的一種方法,那麼我們透過一段程式碼來快速回顧一下這個方法。
typeof 1 // 'number'typeof '1' // 'string'typeof undefined // 'undefined'typeof true // 'boolean'typeof Symbol() // 'symbol'typeof null // 'object'typeof [] // 'object'typeof {} // 'object'typeof console // 'object'typeof console.log // 'function'
你可以看到,前 6 個都是基礎資料型別,而為什麼第 6 個 null 的 typeof 是 'object' 呢?這裡要和你強調一下,雖然 typeof null 會輸出 object,但這只是 JS 存在的一個悠久 Bug,不代表 null 就是引用資料型別,並且 null 本身也不是物件。因此,null 在 typeof 之後返回的是有問題的結果,不能作為判斷 null 的方法。如果你需要在 if 語句中判斷是否為 null,直接透過 ‘===null’來判斷就好。
此外還要注意,引用資料型別 Object,用 typeof 來判斷的話,除了 function 會判斷為 OK 以外,其餘都是 'object',是無法判斷出來的。
第二種判斷方法:instanceof
想必 instanceof 的方法你也聽說過,我們 new 一個物件,那麼這個新物件就是它原型鏈繼承上面的物件了,透過 instanceof 我們能判斷這個物件是否是之前那個建構函式生成的物件,這樣就基本可以判斷出這個新物件的資料型別。下面透過程式碼來了解一下。
let Car = function() {}let benz = new Car()benz instanceof Car // truelet car = new String('Mercedes Benz')car instanceof String // truelet str = 'Covid-19'str instanceof String // false
上面就是用 instanceof 方法判斷資料型別的大致流程,那麼如果讓你自己實現一個 instanceof 的底層實現,應該怎麼寫呢?請看下面的程式碼。
function myInstanceof(left, right) { // 這裡先用typeof來判斷基礎資料型別,如果是,直接返回false if(typeof left !== 'object' || left === null) return false; // getProtypeOf是Object物件自帶的API,能夠拿到引數的原型物件 let proto = Object.getPrototypeOf(left); while(true) { //迴圈往下尋找,直到找到相同的原型物件 if(proto === null) return false; if(proto === right.prototype) return true;//找到相同原型物件,返回true proto = Object.getPrototypeof(proto); }}// 驗證一下自己實現的myInstanceof是否OKconsole.log(myInstanceof(new Number(123), Number)); // trueconsole.log(myInstanceof(123, Number)); // false
現在你知道了兩種判斷資料型別的方法,那麼它們之間有什麼差異呢?我總結了下面兩點:
instanceof 可以準確地判斷複雜引用資料型別,但是不能正確判斷基礎資料型別;
而 typeof 也存在弊端,它雖然可以判斷基礎資料型別(null 除外),但是引用資料型別中,除了 function 型別以外,其他的也無法判斷。
總之,不管單獨用 typeof 還是 instanceof,都不能滿足所有場景的需求,而只能透過二者混寫的方式來判斷。但是這種方式判斷出來的其實也只是大多數情況,並且寫起來也比較難受,你也可以試著寫一下。
其實我個人還是比較推薦下面的第三種方法,相比上述兩個而言,能更好地解決資料型別檢測問題。
第三種判斷方法:Object.prototype.toString
toString() 是 Object 的原型方法,呼叫該方法,可以統一返回格式為 “[object Xxx]” 的字串,其中 Xxx 就是物件的型別。對於 Object 物件,直接呼叫 toString() 就能返回 [object Object];而對於其他物件,則需要透過 call 來呼叫,才能返回正確的型別資訊。我們來看一下程式碼。
Object.prototype.toString({}) // "[object Object]"Object.prototype.toString.call({}) // 同上結果,加上call也okObject.prototype.toString.call(1) // "[object Number]"Object.prototype.toString.call('1') // "[object String]"Object.prototype.toString.call(true) // "[object Boolean]"Object.prototype.toString.call(function(){}) // "[object Function]"Object.prototype.toString.call(null) //"[object Null]"Object.prototype.toString.call(undefined) //"[object Undefined]"Object.prototype.toString.call(/123/g) //"[object RegExp]"Object.prototype.toString.call(new Date()) //"[object Date]"Object.prototype.toString.call([]) //"[object Array]"Object.prototype.toString.call(document) //"[object HTMLDocument]"Object.prototype.toString.call(window) //"[object Window]"
從上面這段程式碼可以看出,Object.prototype.toString.call() 可以很好地判斷引用型別,甚至可以把 document 和 window 都區分開來。
但是在寫判斷條件的時候一定要注意,使用這個方法最後返回統一字串格式為 "[object Xxx]" ,而這裡字串裡面的 "Xxx" ,第一個首字母要大寫(注意:使用 typeof 返回的是小寫),這裡需要多加留意。
那麼下面來實現一個全域性通用的資料型別判斷方法,來加深你的理解,程式碼如下。
function getType(obj){ let type = typeof obj; if (type !== "object") { // 先進行typeof判斷,如果是基礎資料型別,直接返回 return type; } // 對於typeof返回結果是object的,再進行如下的判斷,正則返回結果 return Object.prototype.toString.call(obj).replace(/^[object (\S+)]$/, '$1'); // 注意正則中間有個空格}/ 程式碼驗證,需要注意大小寫,哪些是typeof判斷,哪些是toString判斷?思考下 /getType([]) // "Array" typeof []是object,因此toString返回getType('123') // "string" typeof 直接返回getType(window) // "Window" toString返回getType(null) // "Null"首字母大寫,typeof null是object,需toString來判斷getType(undefined) // "undefined" typeof 直接返回getType() // "undefined" typeof 直接返回getType(function(){}) // "function" typeof能判斷,因此首字母小寫getType(/123/g) //"RegExp" toString返回
下面我們來看本講的最後一部分:資料型別的轉換。
資料型別轉換
在日常的業務開發中,經常會遇到 JavaScript 資料型別轉換問題,有的時候需要我們主動進行強制轉換,而有的時候 JavaScript 會進行隱式轉換,隱式轉換的時候就需要我們多加留心。
那麼這部分都會涉及哪些內容呢?我們先看一段程式碼,瞭解下大致的情況。
'123' == 123 // false or true?'' == null // false or true?'' == 0 // false or true?[] == 0 // false or true?[] == '' // false or true?[] == ![] // false or true?null == undefined // false or true?Number(null) // 返回什麼?Number('') // 返回什麼?parseInt(''); // 返回什麼?{}+10 // 返回什麼?let obj = { Symbol.toPrimitive { return 200; }, valueOf() { return 300; }, toString() { return 'Hello'; }}console.log(obj + 200); // 這裡打印出來是多少?
上面這 12 個問題相信你並不陌生,基本涵蓋了我們平常容易疏漏的一些情況,這就是在做資料型別轉換時經常會遇到的強制轉換和隱式轉換的方式,那麼下面我就圍繞資料型別的兩種轉換方式詳細講解一下,希望可以為你提供一些借鑑。
強制型別轉換
強制型別轉換方式包括 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),這幾種方法都比較類似,透過字面意思可以很容易理解,都是透過自身的方法來進行資料型別的強制轉換。下面我列舉一些來詳細說明。
上面程式碼中,第 8 行的結果是 0,第 9 行的結果同樣是 0,第 10 行的結果是 NaN。這些都是很明顯的強制型別轉換,因為用到了 Number() 和 parseInt()。
其實上述幾個強制型別轉換的原理大致相同,下面我挑兩個比較有代表性的方法進行講解。
Number() 方法的強制轉換規則
如果是布林值,true 和 false 分別被轉換為 1 和 0;如果是數字,返回自身;如果是 null,返回 0;如果是 undefined,返回 NaN;如果是字串,遵循以下規則:如果字串中只包含數字(或者是 0X / 0x 開頭的十六進位制數字字串,允許包含正負號),則將其轉換為十進位制;如果字串中包含有效的浮點格式,將其轉換為浮點數值;如果是空字串,將其轉換為 0;如果不是以上格式的字串,均返回 NaN;
如果是 Symbol,丟擲錯誤;
如果是物件,並且部署了 [Symbol.toPrimitive] ,那麼呼叫此方法,否則呼叫物件的 valueOf() 方法,然後依據前面的規則轉換返回的值;如果轉換的結果是 NaN ,則呼叫物件的 toString() 方法,再次依照前面的順序轉換返回對應的值(Object 轉換規則會在下面細講)。
下面透過一段程式碼來說明上述規則。
Number(true); // 1Number(false); // 0Number('0111'); //111Number(null); //0Number(''); //0Number('1a'); //NaNNumber(-0X11); //-17Number('0X11') //17
其中,我分別列舉了比較常見的 Number 轉換的例子,它們都會把對應的非數字型別轉換成數字型別,而有一些實在無法轉換成數字的,最後只能輸出 NaN 的結果。
Boolean() 方法的強制轉換規則
這個方法的規則是:除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 轉換出來是 false,其他都是 true。
這個規則應該很好理解,沒有那麼多條條框框,我們還是透過程式碼來形成認知,如下所示。
Boolean(0) //falseBoolean(null) //falseBoolean(undefined) //falseBoolean(NaN) //falseBoolean(1) //trueBoolean(13) //trueBoolean('12') //true
其餘的 parseInt()、parseFloat()、toString()、String() 這幾個方法,你可以按照我的方式去整理一下規則,在這裡不佔過多篇幅了。
隱式型別轉換
凡是透過邏輯運算子 (&&、 ||、 !)、運算子 (+、-、*、/)、關係運算子 (>、 <、 <= 、>=)、相等運算子 (==) 或者 if/while 條件的操作,如果遇到兩個資料型別不一樣的情況,都會出現隱式型別轉換。這裡你需要重點關注一下,因為比較隱蔽,特別容易讓人忽視。
下面著重講解一下日常用得比較多的“==”和“+”這兩個符號的隱式轉換規則。
'==' 的隱式型別轉換規則如果型別相同,無須進行型別轉換;如果其中一個操作值是 null 或者 undefined,那麼另一個運算子必須為 null 或者 undefined,才會返回 true,否則都返回 false;如果其中一個是 Symbol 型別,那麼返回 false;兩個操作值如果為 string 和 number 型別,那麼就會將字串轉換為 number;如果一個操作值是 boolean,那麼轉換成 number;如果一個操作值為 object 且另一方為 string、number 或者 symbol,就會把 object 轉為原始型別再進行判斷(呼叫 object 的 valueOf/toString 方法進行轉換)。
如果直接死記這些理論會有點懵,我們還是直接看程式碼,這樣更容易理解一些,如下所示。
null == undefined // true 規則2null == 0 // false 規則2'' == null // false 規則2'' == 0 // true 規則4 字串轉隱式轉換成Number之後再對比'123' == 123 // true 規則4 字串轉隱式轉換成Number之後再對比0 == false // true e規則 布林型隱式轉換成Number之後再對比1 == true // true e規則 布林型隱式轉換成Number之後再對比var a = { value: 0, valueOf: function() { this.value++; return this.value; }};
// 注意這裡a又可以等於1、2、3
console.log(a == 1 && a == 2 && a 3); //true f規則 Object隱式轉換// 注:但是執行過3遍之後,再重新執行a3或之前的數字就是false,因為value已經加上去了,這裡需要注意一下
對照著這個規則看完上面的程式碼和註解之後,你可以再回過頭做一下我在講解“資料型別轉換”之前的那 12 道題目,是不是就很容易解決了?
'+' 的隱式型別轉換規則
'+' 號運算子,不僅可以用作數字相加,還可以用作字串拼接。僅當 '+' 號兩邊都是數字時,進行的是加法運算;如果兩邊都是字串,則直接拼接,無須進行隱式型別轉換。
除了上述比較常規的情況外,還有一些特殊的規則,如下所示。
如果其中有一個是字串,另外一個是 undefined、null 或布林型,則呼叫 toString() 方法進行字串拼接;如果是純物件、陣列、正則等,則預設呼叫物件的轉換方法會存在優先順序(下一講會專門介紹),然後再進行拼接。
如果其中有一個是數字,另外一個是 undefined、null、布林型或數字,則會將其轉換成數字進行加法運算,物件的情況還是參考上一條規則。
如果其中一個是字串、一個是數字,則按照字串規則進行拼接。
下面還是結合程式碼來理解上述規則,如下所示。
1 + 2 // 3 常規情況'1' + '2' // '12' 常規情況// 下面看一下特殊情況'1' + undefined // "1undefined" 規則1,undefined轉換字串'1' + null // "1null" 規則1,null轉換字串'1' + true // "1true" 規則1,true轉換字串'1' + 1n // '11' 比較特殊字串和BigInt相加,BigInt轉換為字串1 + undefined // NaN 規則2,undefined轉換數字相加NaN1 + null // 1 規則2,null轉換為01 + true // 2 規則2,true轉換為1,二者相加為21 + 1n // 錯誤 不能把BigInt和Number型別直接混合相加'1' + 3 // '13' 規則3,字串拼接
整體來看,如果資料中有字串,JavaScript 型別轉換還是更傾向於轉換成字串,因為第三條規則中可以看到,在字串和數字相加的過程中最後返回的還是字串,這裡需要關注一下。
瞭解了 '+' 的轉換規則後,我們最後再看一下 Object 的轉換規則。
Object 的轉換規則
物件轉換的規則,會先呼叫內建的 [ToPrimitive] 函式,其規則邏輯如下:
如果部署了 Symbol.toPrimitive 方法,優先呼叫再返回;呼叫 valueOf(),如果轉換為基礎型別,則返回;呼叫 toString(),如果轉換為基礎型別,則返回;如果都沒有返回基礎型別,會報錯。直接理解有些晦澀,還是直接來看程式碼,你也可以在控制檯自己敲一遍來加深印象。
var obj = { value: 1, valueOf() { return 2; }, toString() { return '3' }, Symbol.toPrimitive { return 4 }}console.log(obj + 1); // 輸出5
// 因為有Symbol.toPrimitive,就優先執行這個;如果Symbol.toPrimitive這段程式碼刪掉,則執行valueOf列印結果為3;如果valueOf也去掉,則呼叫toString返回'31'(字串拼接)
// 再看兩個特殊的case:
10 + {}
// "10[object Object]",注意:{}會預設呼叫valueOf是{},不是基礎型別繼續轉換,呼叫toString,返回結果"[object Object]",於是和10進行'+'運算,按照字串拼接規則來,參考'+'的規則C
[1,2,undefined,4,5] + 10
// "1,2,,4,510",注意[1,2,undefined,4,5]會預設先呼叫valueOf結果還是這個陣列,不是基礎資料型別繼續轉換,也還是呼叫toString,返回"1,2,,4,5",然後再和10進行運算,還是按照字串拼接規則,參考'+'的第3條規則
關於 Object 的轉化,就講解到這裡,希望你可以深刻體會一下上面講的原理和內容。
總結
以上就是本講的內容了,在這一講中,我們從三個方面學習了資料型別相關內容,下面整體回顧一下。
資料型別的基本概念:這是必須掌握的知識點,作為深入理解 JavaScript 的基礎。
資料型別的判斷方法:typeof 和 instanceof,以及 Object.prototype.toString 的判斷資料型別、手寫 instanceof 程式碼片段,這些是日常開發中經常會遇到的,因此你需要好好掌握。
資料型別的轉換方式:兩種資料型別的轉換方式,日常寫程式碼過程中隱式轉換需要多留意,如果理解不到位,很容易引起在編碼過程中的 bug,得到一些意想不到的結果。
下一講我會在本講內容的基礎上,為你詳細介紹手寫一個深淺複製程式碼的完整思路以及程式碼的實現。我們下一講見。