JavaScript 的數(shù)據(jù)類型介紹

數(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,得到一些意想不到的結果。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容