作為
JavaScript
的入門級知識點,
JS
資料類型在整個
JavaScript
的學習過程中其實尤為重要。因為在
JavaScript
程式設計中,我們經常會遇到邊界資料類型條件判斷問題,很多代碼隻有在某種特定的資料類型下,才能可靠地執行。
這一講從資料類型的概念、檢測方法、轉換方法幾個方面,梳理和深入學習 JavaScript 的資料類型的知識點。
希望通過本講的學習,能夠熟練掌握資料類型的判斷以及轉換等相關知識點,并且在遇到資料類型判斷以及資料類型的隐式轉換等問題時可以輕松應對。
資料類型概念
JavaScript
的資料類型一下 8 種:
、
undefined
、
Null
、
Boolean
、
String
、
Number
、
Symbol
、
BigInt
:
Object
、
Array
、
RegExp
、
Date
、
Math
Function
前 7 種類型為基礎類型,最後1種(
Object
)為引用類型,也是需要重點關注的,因為它在日常工作中是使用得最頻繁,也是需要關注最多技術細節的資料類型。
而引用資料類型
(Object)
又分為圖上這幾種常見的類型:
Array
- 數組對象、
RegExp
- 正則對象、
Date
- 日期對象、
Math
- 數學函數、
Function
- 函數對象。
在這裡,重點了解下面兩點因為各種
JavaScript
的資料類型最後都會在初始化之後放在不同的記憶體中,是以上面的資料類型大緻可以分成兩類來進行存儲:
- 基礎類型存儲在棧記憶體,被引用或拷貝時,會建立一個完全相等的變量;
- 引用類型存儲在堆記憶體,存儲的是位址,多個引用指向同一個位址,這裡會涉及一個“共享”的概念。
關于引用類型下面直接通過兩段代碼來講解,深入了解一下核心“共享”的概念。
題目一:初出茅廬
let a = {
name: 'lee',
age: 18
}
let b = a;
console.log(a.name); //第一個console
b.name = 'son';
console.log(a.name); //第二個console
console.log(b.name); //第三個console
這道題比較簡單,可以看到第一個
console
打出來
name
是
'lee'
,這應該沒什麼疑問;但是在執行了
b.name='son'
之後,結果發現
a
和
b
的屬性
name
都是
'son'
,第二個和第三個列印結果是一樣的,這裡就展現了引用類型的“共享”的特性,即這兩個值都存在同一塊記憶體中共享,一個發生了改變,另外一個也随之跟着變化。
題目二:漸入佳境
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); // 第一個console
console.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 行把參數
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 // true
let car = new String('Mercedes Benz')
car instanceof String // true
let 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是否OK
console.log(myInstanceof(new Number(123), Number)); // true
console.log(myInstanceof(123, Number)); // false
現在知道了兩種判斷資料類型的方法,那麼它們之間有什麼差異呢?總結了下面兩點:
-
可以準确地判斷複雜引用資料類型,但是不能正确判斷基礎資料類型;instanceof
-
也存在弊端,它雖然可以判斷基礎資料類型(typeof
除外),但是引用資料類型中,除了null
類型以外,其他的也無法判斷。function
第三種判斷方法:Object.prototype.toString
toString()
是
Object
的原型方法,調用該方法,可以統一傳回格式為
“[object Xxx]”
的字元串,其中
Xxx
就是對象的類型。對于
Object
對象,直接調用
toString()
就能傳回
[object Object]
;而對于其他對象,則需要通過
call
來調用,才能傳回正确的類型資訊。來看一下代碼。
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上結果,加上call也ok
Object.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
行的結果是
,第
9
行的結果同樣是
,第
10
行的結果是
NaN
。這些都是很明顯的強制類型轉換,因為用到了
Number()
和
parseInt()
。
其實上述幾個強制類型轉換的原理大緻相同,下面挑兩個比較有代表性的方法進行講解。
Number() 方法的強制轉換規則
- 如果是布爾值,
和true
分别被轉換為false
和 ;1
- 如果是數字,傳回自身;
- 如果是
,傳回 ;null
- 如果是
,傳回undefined
;NaN
- 如果是字元串,遵循以下規則:如果字元串中隻包含數字(或者是
開頭的十六進制數字字元串,允許包含正負号),則将其轉換為十進制;如果字元串中包含有效的浮點格式,将其轉換為浮點數值;如果是空字元串,将其轉換為 ;如果不是以上格式的字元串,均傳回0X / 0x
;NaN
- 如果是
,抛出錯誤;Symbol
- 如果是對象,并且部署了
,那麼調用此方法,否則調用對象的[Symbol.toPrimitive]
方法,然後依據前面的規則轉換傳回的值;如果轉換的結果是valueOf()
,則調用對象的NaN
方法,再次依照前面的順序轉換傳回對應的值(toString()
轉換規則會在下面細講)。Object
下面通過一段代碼來說明上述規則。
Number(true); // 1
Number(false); // 0
Number('0111'); //111
Number(null); //0
Number(''); //0
Number('1a'); //NaN
Number(-0X11); //-17
Number('0X11') //17
其中,分别列舉了比較常見的
Number
轉換的例子,它們都會把對應的非數字類型轉換成數字類型,而有一些實在無法轉換成數字的,最後隻能輸出
NaN
的結果。
Boolean() 方法的強制轉換規則
這個方法的規則是:除了
undefined
、
null
、
false
、
''
、
(包括
+0
,
-0
)、
NaN
轉換出來是
false
,其他都是
true
。
這個規則應該很好了解,沒有那麼多條條框框,我們還是通過代碼來形成認知,如下所示。
Boolean(0) //false
Boolean(null) //false
Boolean(undefined) //false
Boolean(NaN) //false
Boolean(1) //true
Boolean(13) //true
Boolean('12') //true
隐式類型轉換
凡是通過邏輯運算符 (
&&
、
||
、
!
)、運算符 (
+
、
-
、
*
、
/
)、關系操作符(
>
、
<
、
<=
、
>=
)、相等運算符(
==
)或者
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 規則2
null == 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遍之後,再重新執行a==3或之前的數字就是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轉換數字相加NaN
1 + null // 1 規則2,null轉換為0
1 + true // 2 規則2,true轉換為1,二者相加為2
1 + 1n // 錯誤 不能把BigInt和Number類型直接混合相加
'1' + 3 // '13' 規則3,字元串拼接
整體來看,如果資料中有字元串,
JavaScript
類型轉換還是更傾向于轉換成字元串,因為第三條規則中可以看到,在字元串和數字相加的過程中最後傳回的還是字元串,這裡需要關注一下。
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條規則
總結
從三個方面學習了資料類型相關内容,下面整體回顧一下。
- 資料類型的基本概念:這是必須掌握的知識點,作為深入了解
的基礎。JavaScript
- 資料類型的判斷方法:
和typeof
,以及instanceof
的判斷資料類型、手寫Object.prototype.toString
代碼片段,這些是日常開發中經常會遇到的,是以你需要好好掌握。instanceof
- 資料類型的轉換方式:兩種資料類型的轉換方式,日常寫代碼過程中隐式轉換需要多留意,如果了解不到位,很容易引起在編碼過程中的 bug,得到一些意想不到的結果。