數(shù)據(jù)類型概念
JavaScript 的數(shù)據(jù)類型有下圖所示的 8 種:

其中,前 7 種類型為基礎類型,最后 1 種(Object)為引用類型,也是你需要重點關注的,因為它在日常工作中是使用得最頻繁,也是需要關注最多技術細節(jié)的數(shù)據(jù)類型。
而引用數(shù)據(jù)類型(Object)又分為圖上這幾種常見的類型:Array - 數(shù)組對象、RegExp - 正則對象、Date - 日期對象、Math - 數(shù)學函數(shù)、Function - 函數(shù)對象。
在這里,我想先請你重點了解下面兩點,因為各種 JavaScript 的數(shù)據(jù)類型最后都會在初始化之后放在不同的內(nèi)存中,因此上面的數(shù)據(jù)類型大致可以分成兩類來進行存儲:
1、基礎類型存儲在棧內(nèi)存,被引用或拷貝時,會創(chuàng)建一個完全相等的變量;
2、引用類型存儲在堆內(nèi)存,存儲的是地址,多個引用指向同一個地址,這里會涉及一個“共享”的概念。
關于引用類型下面直接通過兩段代碼來講解,讓你深入理解一下核心“共享”的概念。
題目一:初出茅廬
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',這應該沒什么疑問;但是在執(zhí)行了 b.name='son' 之后,結果你會發(fā)現(xiàn) a 和 b 的屬性 name 都是 'son',第二個和第三個打印結果是一樣的,這里就體現(xiàn)了引用類型的“共享”的特性,即這兩個值都存在同一塊內(nèi)存中共享,一個發(fā)生了改變,另外一個也隨之跟著變化。
你可以直接在 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);? ? // 第一個console
console.log(a.age);? ? // 第二個console
這道題涉及了 function,你通過上述代碼可以看到第一個 console 的結果是 30,b 最后打印結果是 {name: "Kath", age: 30};第二個 console 的返回結果是 24,而 a 最后的打印結果是 {name: "Julia", age: 24}。
是不是和你預想的有些區(qū)別?你要注意的是,這里的 function 和 return 帶來了不一樣的東西。
原因在于:函數(shù)傳參進來的 o,傳遞的是對象在堆中的內(nèi)存地址值,通過調(diào)用 o.age = 24(第 7 行代碼)確實改變了 a 對象的 age 屬性;但是第 12 行代碼的 return 卻又把 o 變成了另一個內(nèi)存地址,將 {name: "Kath", age: 30} 存入其中,最后返回 b 的值就變成了 {name: "Kath", age: 30}。而如果把第 12 行去掉,那么 b 就會返回 undefined。這里你可以再仔細琢磨一下。
講完數(shù)據(jù)類型的基本概念,我們繼續(xù)看下一部分,如何對數(shù)據(jù)類型進行檢測,這也是比較重要的問題。
數(shù)據(jù)類型檢測
數(shù)據(jù)類型檢測也是面試過程中經(jīng)常會遇到的問題,比如:如何判斷是否為數(shù)組?讓你寫一段代碼把 JavaScript 的各種數(shù)據(jù)類型判斷出來,等等。類似的題目會很多,而且在平常寫代碼過程中我們也會經(jīng)常用到。
我也經(jīng)常在面試一些候選人的時候,有些回答比如“用 typeof 來判斷”,然后就沒有其他答案了,但這樣的回答是不能令面試官滿意的,因為他要考察你對 JS 的數(shù)據(jù)類型理解的深度,所以我們先要做到的是對各種數(shù)據(jù)類型的判斷方法了然于胸,然后再進行歸納總結,給面試官一個滿意的答案。
數(shù)據(jù)類型的判斷方法其實有很多種,比如 typeof 和 instanceof,下面我來重點介紹三種在工作中經(jīng)常會遇到的數(shù)據(jù)類型檢測方法。
第一種判斷方法: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 個都是基礎數(shù)據(jù)類型,而為什么第 6 個 null 的 typeof 是 'object' 呢?這里要和你強調(diào)一下,雖然 typeof null 會輸出 object,但這只是 JS 存在的一個悠久 Bug,不代表 null 就是引用數(shù)據(jù)類型,并且 null 本身也不是對象。因此,null 在 typeof 之后返回的是有問題的結果,不能作為判斷 null 的方法。如果你需要在 if 語句中判斷是否為 null,直接通過 ‘===null’來判斷就好。
此外還要注意,引用數(shù)據(jù)類型 Object,用 typeof 來判斷的話,除了 function 會判斷為 OK 以外,其余都是 'object',是無法判斷出來的。
第二種判斷方法:instanceof
想必 instanceof 的方法你也聽說過,我們 new 一個對象,那么這個新對象就是它原型鏈繼承上面的對象了,通過 instanceof 我們能判斷這個對象是否是之前那個構造函數(shù)生成的對象,這樣就基本可以判斷出這個新對象的數(shù)據(jù)類型。下面通過代碼來了解一下。
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 方法判斷數(shù)據(jù)類型的大致流程,那么如果讓你自己實現(xiàn)一個 instanceof 的底層實現(xiàn),應該怎么寫呢?請看下面的代碼。
function myInstanceof(left, right) {
? // 這里先用typeof來判斷基礎數(shù)據(jù)類型,如果是,直接返回false
? if(typeof left !== 'object' || left === null) return false;
? // getProtypeOf是Object對象自帶的API,能夠拿到參數(shù)的原型對象
? let proto = Object.getPrototypeOf(left);
? while(true) {? ? ? ? ? ? ? ? ? //循環(huán)往下尋找,直到找到相同的原型對象
? ? if(proto === null) return false;
? ? if(proto === right.prototype) return true;//找到相同原型對象,返回true
? ? proto = Object.getPrototypeof(proto);
? ? }
}
// 驗證一下自己實現(xiàn)的myInstanceof是否OK
console.log(myInstanceof(new Number(123), Number));? ? // true
console.log(myInstanceof(123, Number));? ? ? ? ? ? ? ? // false
現(xiàn)在你知道了兩種判斷數(shù)據(jù)類型的方法,那么它們之間有什么差異呢?我總結了下面兩點:
instanceof 可以準確地判斷復雜引用數(shù)據(jù)類型,但是不能正確判斷基礎數(shù)據(jù)類型;
而 typeof 也存在弊端,它雖然可以判斷基礎數(shù)據(jù)類型(null 除外),但是引用數(shù)據(jù)類型中,除了 function 類型以外,其他的也無法判斷。
總之,不管單獨用 typeof 還是 instanceof,都不能滿足所有場景的需求,而只能通過二者混寫的方式來判斷。但是這種方式判斷出來的其實也只是大多數(shù)情況,并且寫起來也比較難受,你也可以試著寫一下。
其實我個人還是比較推薦下面的第三種方法,相比上述兩個而言,能更好地解決數(shù)據(jù)類型檢測問題。
第三種判斷方法:Object.prototype.toString
toString() 是 Object 的原型方法,調(diào)用該方法,可以統(tǒng)一返回格式為 “[object Xxx]” 的字符串,其中 Xxx 就是對象的類型。對于 Object 對象,直接調(diào)用 toString() 就能返回 [object Object];而對于其他對象,則需要通過 call 來調(diào)用,才能返回正確的類型信息。我們來看一下代碼。
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 都區(qū)分開來。
但是在寫判斷條件的時候一定要注意,使用這個方法最后返回統(tǒng)一字符串格式為 "[object Xxx]" ,而這里字符串里面的 "Xxx" ,第一個首字母要大寫(注意:使用 typeof 返回的是小寫),這里需要多加留意。
那么下面來實現(xiàn)一個全局通用的數(shù)據(jù)類型判斷方法,來加深你的理解,代碼如下。
function getType(obj){
? let type? = typeof obj;
? if (type !== "object") {? ? // 先進行typeof判斷,如果是基礎數(shù)據(jù)類型,直接返回
? ? 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返回
到這里,數(shù)據(jù)類型檢測的三種方法就介紹完了,最后也給出來了示例代碼,希望你可以對比著來學習、使用,并且不斷加深記憶,以便遇到問題時不會手忙腳亂。你如果一遍記不住可以多次來回看鞏固,直到把上面的代碼都能全部理解,并且把幾個特殊的問題都強化記憶,這樣未來你去做類似題目才不會有問題。
下面我們來看本講的最后一部分:數(shù)據(jù)類型的轉換。
數(shù)據(jù)類型轉換
在日常的業(yè)務開發(fā)中,經(jīng)常會遇到 JavaScript 數(shù)據(jù)類型轉換問題,有的時候需要我們主動進行強制轉換,而有的時候 JavaScript 會進行隱式轉換,隱式轉換的時候就需要我們多加留心。
那么這部分都會涉及哪些內(nèi)容呢?我們先看一段代碼,了解下大致的情況。
'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 個問題相信你并不陌生,基本涵蓋了我們平常容易疏漏的一些情況,這就是在做數(shù)據(jù)類型轉換時經(jīng)常會遇到的強制轉換和隱式轉換的方式,那么下面我就圍繞數(shù)據(jù)類型的兩種轉換方式詳細講解一下,希望可以為你提供一些借鑒。
強制類型轉換
強制類型轉換方式包括 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),這幾種方法都比較類似,通過字面意思可以很容易理解,都是通過自身的方法來進行數(shù)據(jù)類型的強制轉換。下面我列舉一些來詳細說明。
上面代碼中,第 8 行的結果是 0,第 9 行的結果同樣是 0,第 10 行的結果是 NaN。這些都是很明顯的強制類型轉換,因為用到了 Number() 和 parseInt()。
其實上述幾個強制類型轉換的原理大致相同,下面我挑兩個比較有代表性的方法進行講解。
Number() 方法的強制轉換規(guī)則
·如果是布爾值,true 和 false 分別被轉換為 1 和 0;
·如果是數(shù)字,返回自身;
·如果是 null,返回 0;
·如果是 undefined,返回?NaN;
·如果是字符串,遵循以下規(guī)則:如果字符串中只包含數(shù)字(或者是?0X?/?0x?開頭的十六進制數(shù)字字符串,允許包含正負號),則將其轉換為十進制;如果字符串中包含有效的浮點格式,將其轉換為浮點數(shù)值;如果是空字符串,將其轉換為 0;如果不是以上格式的字符串,均返回?NaN;
·如果是 Symbol,拋出錯誤;
·如果是對象,并且部署了 [Symbol.toPrimitive] ,那么調(diào)用此方法,否則調(diào)用對象的?valueOf()?方法,然后依據(jù)前面的規(guī)則轉換返回的值;如果轉換的結果是?NaN?,則調(diào)用對象的?toString()?方法,再次依照前面的順序轉換返回對應的值(Object 轉換規(guī)則會在下面細講)。
下面通過一段代碼來說明上述規(guī)則。
Number(true);? ? ? ? // 1
Number(false);? ? ? // 0
Number('0111');? ? ? //111
Number(null);? ? ? ? //0
Number('');? ? ? ? ? //0
Number('1a');? ? ? ? //NaN
Number(-0X11);? ? ? //-17
Number('0X11')? ? ? //17
其中,我分別列舉了比較常見的 Number 轉換的例子,它們都會把對應的非數(shù)字類型轉換成數(shù)字類型,而有一些實在無法轉換成數(shù)字的,最后只能輸出 NaN 的結果。
Boolean() 方法的強制轉換規(guī)則
這個方法的規(guī)則是:除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 轉換出來是 false,其他都是 true。
這個規(guī)則應該很好理解,沒有那么多條條框框,我們還是通過代碼來形成認知,如下所示。
Boolean(0)? ? ? ? ? //false
Boolean(null)? ? ? //false
Boolean(undefined)? //false
Boolean(NaN)? ? ? ? //false
Boolean(1)? ? ? ? ? //true
Boolean(13)? ? ? ? //true
Boolean('12')? ? ? //true
其余的 parseInt()、parseFloat()、toString()、String() 這幾個方法,你可以按照我的方式去整理一下規(guī)則,在這里不占過多篇幅了。
隱式類型轉換
凡是通過邏輯運算符 (&&、 ||、 !)、運算符 (+、-、*、/)、關系操作符 (>、 <、 <= 、>=)、相等運算符 (==) 或者 if/while 條件的操作,如果遇到兩個數(shù)據(jù)類型不一樣的情況,都會出現(xiàn)隱式類型轉換。這里你需要重點關注一下,因為比較隱蔽,特別容易讓人忽視。
下面著重講解一下日常用得比較多的“==”和“+”這兩個符號的隱式轉換規(guī)則。
'==' 的隱式類型轉換規(guī)則
·如果類型相同,無須進行類型轉換;
·如果其中一個操作值是 null 或者 undefined,那么另一個操作符必須為 null 或者 undefined,才會返回 true,否則都返回 false;
·如果其中一個是 Symbol 類型,那么返回 false;
·兩個操作值如果為 string 和 number 類型,那么就會將字符串轉換為 number;
·如果一個操作值是 boolean,那么轉換成 number;
·如果一個操作值為 object 且另一方為 string、number 或者 symbol,就會把 object 轉為原始類型再進行判斷(調(diào)用 object 的 valueOf/toString 方法進行轉換)。
如果直接死記這些理論會有點懵,我們還是直接看代碼,這樣更容易理解一些,如下所示。
null == undefined? ? ? // true? 規(guī)則2
null == 0? ? ? ? ? ? ? // false 規(guī)則2
'' == null? ? ? ? ? ? ? // false 規(guī)則2
'' == 0? ? ? ? ? ? ? ? // true? 規(guī)則4 字符串轉隱式轉換成Number之后再對比
'123' == 123? ? ? ? ? ? // true? 規(guī)則4 字符串轉隱式轉換成Number之后再對比
0 == false? ? ? ? ? ? ? // true? e規(guī)則 布爾型隱式轉換成Number之后再對比
1 == true? ? ? ? ? ? ? // true? e規(guī)則 布爾型隱式轉換成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規(guī)則 Object隱式轉換
// 注:但是執(zhí)行過3遍之后,再重新執(zhí)行a==3或之前的數(shù)字就是false,因為value已經(jīng)加上去了,這里需要注意一下
對照著這個規(guī)則看完上面的代碼和注解之后,你可以再回過頭做一下我在講解“數(shù)據(jù)類型轉換”之前的那 12 道題目,是不是就很容易解決了?
'+' 的隱式類型轉換規(guī)則
'+' 號操作符,不僅可以用作數(shù)字相加,還可以用作字符串拼接。僅當 '+' 號兩邊都是數(shù)字時,進行的是加法運算;如果兩邊都是字符串,則直接拼接,無須進行隱式類型轉換。
除了上述比較常規(guī)的情況外,還有一些特殊的規(guī)則,如下所示。
·如果其中有一個是字符串,另外一個是 undefined、null 或布爾型,則調(diào)用 toString() 方法進行字符串拼接;如果是純對象、數(shù)組、正則等,則默認調(diào)用對象的轉換方法會存在優(yōu)先級(下一講會專門介紹),然后再進行拼接。
·如果其中有一個是數(shù)字,另外一個是 undefined、null、布爾型或數(shù)字,則會將其轉換成數(shù)字進行加法運算,對象的情況還是參考上一條規(guī)則。
·如果其中一個是字符串、一個是數(shù)字,則按照字符串規(guī)則進行拼接。
下面還是結合代碼來理解上述規(guī)則,如下所示。
1 + 2? ? ? ? // 3? 常規(guī)情況
'1' + '2'? ? // '12' 常規(guī)情況
// 下面看一下特殊情況
'1' + undefined? // "1undefined" 規(guī)則1,undefined轉換字符串
'1' + null? ? ? ? // "1null" 規(guī)則1,null轉換字符串
'1' + true? ? ? ? // "1true" 規(guī)則1,true轉換字符串
'1' + 1n? ? ? ? ? // '11' 比較特殊字符串和BigInt相加,BigInt轉換為字符串
1 + undefined? ? // NaN? 規(guī)則2,undefined轉換數(shù)字相加NaN
1 + null? ? ? ? ? // 1? ? 規(guī)則2,null轉換為0
1 + true? ? ? ? ? // 2? ? 規(guī)則2,true轉換為1,二者相加為2
1 + 1n? ? ? ? ? ? // 錯誤? 不能把BigInt和Number類型直接混合相加
'1' + 3? ? ? ? ? // '13' 規(guī)則3,字符串拼接
整體來看,如果數(shù)據(jù)中有字符串,JavaScript 類型轉換還是更傾向于轉換成字符串,因為第三條規(guī)則中可以看到,在字符串和數(shù)字相加的過程中最后返回的還是字符串,這里需要關注一下。
了解了 '+' 的轉換規(guī)則后,我們最后再看一下 Object 的轉換規(guī)則。
Object 的轉換規(guī)則
對象轉換的規(guī)則,會先調(diào)用內(nèi)置的 [ToPrimitive] 函數(shù),其規(guī)則邏輯如下:
如果部署了 Symbol.toPrimitive 方法,優(yōu)先調(diào)用再返回;
調(diào)用 valueOf(),如果轉換為基礎類型,則返回;
調(diào)用 toString(),如果轉換為基礎類型,則返回;
如果都沒有返回基礎類型,會報錯。
直接理解有些晦澀,還是直接來看代碼,你也可以在控制臺自己敲一遍來加深印象。
var obj = {
? value: 1,
? valueOf() {
? ? return 2;
? },
? toString() {
? ? return '3'
? },
? [Symbol.toPrimitive]() {
? ? return 4
? }
}
console.log(obj + 1); // 輸出5
// 因為有Symbol.toPrimitive,就優(yōu)先執(zhí)行這個;如果Symbol.toPrimitive這段代碼刪掉,則執(zhí)行valueOf打印結果為3;如果valueOf也去掉,則調(diào)用toString返回'31'(字符串拼接)
// 再看兩個特殊的case:
10 + {}
// "10[object Object]",注意:{}會默認調(diào)用valueOf是{},不是基礎類型繼續(xù)轉換,調(diào)用toString,返回結果"[object Object]",于是和10進行'+'運算,按照字符串拼接規(guī)則來,參考'+'的規(guī)則C
[1,2,undefined,4,5] + 10
// "1,2,,4,510",注意[1,2,undefined,4,5]會默認先調(diào)用valueOf結果還是這個數(shù)組,不是基礎數(shù)據(jù)類型繼續(xù)轉換,也還是調(diào)用toString,返回"1,2,,4,5",然后再和10進行運算,還是按照字符串拼接規(guī)則,參考'+'的第3條規(guī)則
關于 Object 的轉化,就講解到這里,希望你可以深刻體會一下上面講的原理和內(nèi)容。
總結
以上就是本講的內(nèi)容了,在這一講中,我們從三個方面學習了數(shù)據(jù)類型相關內(nèi)容,下面整體回顧一下。
1、數(shù)據(jù)類型的基本概念:這是必須掌握的知識點,作為深入理解 JavaScript 的基礎。
2、數(shù)據(jù)類型的判斷方法:typeof 和 instanceof,以及 Object.prototype.toString 的判斷數(shù)據(jù)類型、手寫 instanceof 代碼片段,這些是日常開發(fā)中經(jīng)常會遇到的,因此你需要好好掌握。
3、數(shù)據(jù)類型的轉換方式:兩種數(shù)據(jù)類型的轉換方式,日常寫代碼過程中隱式轉換需要多留意,如果理解不到位,很容易引起在編碼過程中的 bug,得到一些意想不到的結果。