App下載

初中級前端必須要知道的JS數據類型

猿友 2020-10-13 11:38:33 瀏覽數 (3312)
反饋

JavaScript中有哪些數據類型?

計算機世界中定義的數據類型其實就是為了描述現實世界中存在的事實而定義的。比如我們用人來舉例:

  1. 有沒有人在房間里?這里的有和沒有就是是或者非的概念,在 JS 中對應 Boolean 類型, true 表示是, false 表示非;
  2. 有幾個人在房間里?這里的幾個表示的是一個量級概念,在 JS 中對應 Number 類型,包含整數和浮點數,還有一些特殊的值,比如: -Infinity 表示負無窮大、 +Infinity 表示正無窮大、 NaN 表示不是一個數字;
  3. 房間里的這些人都是我的朋友。這是一句陳述語句,這種文本類的信息將會以字符串形式進行存儲,在 JS 中對應 String 類型;
  4. 房間里沒有人。這里的沒有代表無和空的概念,在 JSnullundefined 都可以表示這個意思;
  5. 現實世界中所有人都是獨一無二的,這在 JS 中對應 Symbol 類型,表示唯一且不可改變;
  6. Number 所表示的整數是有范圍的,超出范圍的數據就沒法用 Number 表示了,于是 ES10 中提出了一種新的數據類型 BigInt,能表示任何位數的整數;
  7. 以上提到的 Boolean、 Number、 String、 null、 undefined、 SymbolBigInt 等7種類型都是 JavaScript 中的原始類型,還有一種是非原始類型叫做對象類型;比如:一個人是對象,這個人有名字、性別、年齡等;

let person = {
    name: 'bubuzou',
    sex: 'male',
    age: 26,}

為什么要區(qū)分原始類型和對象類型?他們之間有什么區(qū)別?

原始類型的不可變性

在回答這個問題之前,我們先看一下變量在內存中是如何存儲的:

let name1 = 'bubuzou'
let name2 = name1.concat('.com')
console.log(name1)  // 'bubuzou'

執(zhí)行完上面這段代碼,我們發(fā)現變量 name1 的值還是不變,依然是 bubuzou。這就說明了字符串的不可變性。但是你看了下面的這段代碼,你就會產生疑問了:

let name1 = 'bubuzou'
name1 += '.com'
console.log(name1)  // 'bubuzou.com'

你說字符串是不可變的,那現在不是變了嘛? 其實這只是變量的值變了,但是存在內存中的字符串依然不變。這就涉及到變量在內存中的存儲了。 在 JavaScript 中,變量在內存中有2種存儲方式:存在棧中和存在堆中。那么棧內存和堆內存有啥區(qū)別呢?

棧內存:

  • 順序存儲結構,特點是先進后出。就像一個兵乒球盒子一樣,兵乒球從外面一個個的放入盒子里,最先取出來的一定是最后放入盒子的那個。
  • 存儲空間固定
  • 可以直接操作其保存的值,執(zhí)行效率高

堆內存:

  • 無序的存儲結構
  • 存儲空間可以動態(tài)變化
  • 無法直接操作其內部的存儲,需要通過引用地址操作

了解完變量在內存中的存儲方式有2種,那我們繼續(xù)以上面那串代碼為例,畫出變量的存儲結構圖:

變量的存儲結構

然后我們可以描述下當計算機執(zhí)行這段代碼時候的發(fā)生了什么?首先定義了一個變量 name1 并且給其賦值 bubuzou 這個時候就會在內存中開辟一塊空間用來存儲字符串 bubuzou,然后變量指向了這個內存空間。然后再執(zhí)行第二行代碼 letname2=name1.concat('.com') 這里的拼接操作其實是產生了一個新字符串 bubuzou.com,所以又會為這個新字符串創(chuàng)建一塊新內存,并且把定義的變量 name2 指向這個內存地址。 所以我們看到其實整個操作 bubuzou 這個字符串所在的內存其實是沒有變化的,即使在第二段代碼中執(zhí)行了 name1+='.com' 操作,其實也只是變量 name1 指向了新的字符串 bubuzou.com 而已,舊的字符串 bubuzou 依然存在內存中,不過一段時間后由于該字符串沒有被變量所引用,所以會被當成垃圾進行回收,從而釋放掉該塊內存空間。

從而我們得出結論:原始類型的值都是固定的,而對象類型則是由原始類型的鍵值對組合成一個復雜的對象;他們在內存中的存儲方式是不一樣的,原始類型的值直接存在棧內存中,而對象類型的實際值是存在堆內存中的,在棧內存中保存了一份引用地址,這個地址指向堆內存中的實際值,所以對象類型又習慣被叫做引用類型。

想一個問題為什么引用類型的值要存儲到堆內存中?能不能存到棧內存中呢?答案一:因為引用類型大小不固定,而棧的大小是固定的,堆空間的大小是可以動態(tài)變化的,所以引用類型的值適合存在堆中;答案二:在代碼執(zhí)行過程中需要頻繁的切換執(zhí)行上下文的時候,如果把引用類型的值存到棧中,將會造成非常大的內存開銷。

比較

當我們對兩個變量進行比較的時候,不同類型的變量是有不同表現的:

let str1 = 'hello'
let str2 = 'hello'
console.log( str1 === str2 ) // true
let person1 = {
    name: 'bubuzou'
}
let person2 = {
    name: 'bubuzou'
}
console.log( person1 === person2 )  // false

我們定義了2個字符串變量和2個對象變量,他們都長一模一樣,但是字符串變量會相等,對象變量卻不相等。這是因為在 JavaScript 中,原型類型進行比較的時候比較的是存在棧中的值是否相等;而引用類型進行比較的時候,是比較棧內存中的引用地址是否相等。 如上幾個變量在內存中的存儲模型如圖所示:

棧內存

復制

變量進行復制的時候,原始類型和引用類型變量也是有區(qū)別的,來看下面的代碼:

let str1 = 'hello'
let str2 = str1
str2 = 'world'
console.log( str1 ) // 'hello'

復制

  1. letstr1='hello': 復制前,定義了一個變量 str1,并且給其賦值 hello,這個時候 hello 這個字符串就會在棧內存中被分配一塊空間進行存儲,然后變量 str1 會指向這個內存地址;
  2. letstr2=str1:復制后,把 str1 的值賦值給 str2,這個時候會在棧中新開辟一塊空間用來存儲 str2 的值;
  3. str2='world':給 str2 賦值了一個新的字符串 world,那么將新建一塊內存用來存儲 world,同時 str2 原來的值 hello 的內存空間因為沒有變量所引用,所以一段時間后將被當成垃圾回收;
  4. console.log(str1):因為 str1str2 的棧內存地址是不一樣的,所以即使 str2 的值被改變,也不會影響到 str1。

然后我們繼續(xù)往下,看下引用類型的復制:

let person1 = {
    name: 'bubuzou',
    age: 20
}
let person2 = person1
person2.name = 'bubuzou.com'
console.log( person1.name)  // 'bubuzou.com'

引用類型的復制

原始類型進行復制的時候是變量的值進行重新賦值,而如上圖所示:引用類型進行復制的時候是把變量所指向的引用地址進行賦值給新的變量,所以復制后 person1person2 都指向堆內存中的同一個值,所以當改變 person2.name 的時候, person1.name 也會被改變就是這個原因。

值傳遞和引用傳遞

先說一下結論,在 JavaScript 中,所有函數的參數傳遞都是按值進行傳遞的??慈缦麓a:

let name = 'bubuzou'
function changeName(name) {
    name = 'bubuzou.com'
}
changeName(name)
console.log( name )  // 'bubuzou'

定義了一個變量 name,并賦值為 bubuzou,函數調用的時候傳入 name,這個時候會在函數內部創(chuàng)建一個局部變量 name 并且把全局變量的值 bubuzou 傳遞給他,這個操作其實是在內存里新建了一塊空間用來存放局部變量的值,然后又把局部變量的值改成了 bubuzou.com,這個時候其實內存中會有3塊地址空間分別用來存放全局變量的值 bubuzou、局部變量原來的值 bubuzou、和局部變量新的值 bubuzou.com;一旦函數調用結束,局部變量將被銷毀,一段時間后由于局部變量新舊值沒有變量引用,那這兩塊空間將被回收釋放;所以這個時候全局 name 的值依然是 bubuzou

再來看看引用類型的傳參,會不會有所不同呢?

let person = {
    name: 'bubuzou'
}
function changePerosn(person) {
    person.name = 'bubuzou.com'
}
changePerosn( person )
console.log( person.name )  // 'bubuzou.com'

引用類型進行函數傳參的時候,會把引用地址復制給局部變量,所以全局的 person 和函數內部的局部變量 person 是指向同一個堆地址的,所以一旦一方改變,另一方也將被改變,所以至此我們是不是可以下結論說:當函數進行傳參的時候如果參數是引用類型那么就是引用傳遞嘛?

將上面的例子改造下:

let person = {
    name: 'bubuzou'
}
function changePerosn(person) {
    person.name = 'bubuzou.com'
    person = {        name: 'hello world'    
    }
}
changePerosn( person )
console.log( person.name )  // 'bubuzou.com'

如果 person 是引用傳遞的話,那就會自動指向值被改為 hello world 的新對象;事實上全局變量 person 的引用地址自始至終都沒有改變,倒是局部變量 person 的引用地址發(fā)生了改變。

null 和 undefined 傻傻分不清?

nullJavaScript 中自成一種原始類型,只有一個值 null,表示無、空、值未知等特殊值??梢灾苯咏o一個變量賦值為 null

let s = null

undefinednull 一樣也是自成一種原始類型,表示定義了一個變量,但是沒有賦值,則這個變量的值就是 undefined:

let s
console.log( s)  // undefined

雖然可以給變量直接賦值為 undefined 也不會報錯,但是原則上如果一個變量值未定,或者表示空,則直接賦值為 null 比較合適,不建議給變量賦值 undefined。 nullundefined 在進行邏輯判斷的時候都是會返回 false 的:

let a = null, b
console.log( a ? 'a' : b ? 'b' : 'c') // 'c'

null 在轉成數字類型的時候會變成 0,而 undefined 會變成 NaN:

let a = null, b
console.log( +null )  // 0
console.log( + b )  // NaN

認識新的原始類型 Symbol

Symbol 值表示唯一標識符,是 ES6 中新引進的一種原始類型。可以通過 Symbol() 來創(chuàng)建一個重要的值,也可以傳入描述值;其唯一性體現在即使是傳入一樣的描述,他們兩者之間也是不會相等的:

let a = Symbol('bubuzou')
let b = Symbol('bubuzou')
console.log( a === b )  // false

全局的 Symbol

那還是不是任意2個描述一樣的 Symbol 都是不相等的呢?答案是否定的??梢酝ㄟ^ Symbol.for() 來查找或新建一個 Symbol

let a = Symbol.for('bubuzou')
let b = Symbol.for('bubuzou')
console.log( a === b )  // true

使用 Symbol.for() 可以在根據傳入的描述在全局范圍內進行查找,如果沒找到則新建一個 Symbol,并且返回;所以當執(zhí)行第二行代碼 Symbol.for('bubuzou') 的時候,就會找到全局的那個描述為 bubuzouSymbol,所以這里 ab 是會絕對相等的。

居然可以通過描述找到 Symbol, 那是否可以通過 Symbol 來找到描述呢?答案是肯定的,但是必須是全局的 Symbol,如果沒找到則會返回 undefined:

let a = Symbol.for('bubuzou')
let desc = Symbol.keyFor( a )
console.log( desc )  // 'bubuzou'

但是對于任何一個 Symbol 都有一個屬性 description,表示這個 Symbol 的描述:

let a = Symbol('bubuzou')
console.log( a.description )  // 'bubuzou'

Symbol 作為對象屬性

我們知道對象的屬性鍵可以是字符串,但是不能是 Number 或者 Boolean; Symbol 被設計出來其實最大的初衷就是用于對象的屬性鍵:

let age = Symbol('20')
let person = {
    name: 'bubuzou', 
    [age]: '20',  // 在對象字面量中使用 `Symbol` 的時候需要使用中括號包起來
}

這里給 person 定義了一個 Symbol 作為屬性鍵的屬性,這個相比于用字符串作為屬性鍵有啥好處呢?最明顯的好處就是如果這個 person 對象是多個開發(fā)者進行開發(fā)維護,那么很容易再給 person 添加屬性的時候出現同名的,如果是用字符串作為屬性鍵那肯定是沖突了,但是如果用 Symbol 作為屬性鍵,就不會存在這個問題了,因為它是唯一標識符,所以可以使對象的屬性受到保護,不會被意外的訪問或者重寫。

注意一點,如果用 Symbol 作為對象的屬性鍵的時候, forinObject.getOwnPropertyNames、或 Object.keys() 這里循環(huán)是無法獲取 Symbol 屬性鍵的,但是可以通過 Object.getOwnPropertySymbols() 來獲??;在上面的代碼基礎上:

for (let o in person) {
    console.log( o ) // 'name'
}
console.log (Object.keys( person )) // ['name']
console.log(Object.getOwnPropertyNames( person ))  // ['name']
console.log(Object.getOwnPropertySymbols( person ))  // [Symbol(20)]

你可能不知道的 Number 類型

JavaScript 中的數字涉及到了兩種類型:一種是 Number 類型,以 64 位的格式 IEEE-754 存儲,也被稱為雙精度浮點數,就是我們平常使用的數字,其范圍是 $2^{52}$ 到 -$2^{52}$;第二種類型是 BigInt,能夠表示任意長度的整數,包括超出 $2^{52}$ 到 -$2^{52}$ 這個范圍外的數。這里我們只介紹 Number 數字。

常規(guī)數字和特殊數字

對于一個常規(guī)的數字,我們直接寫即可,比如:

let age = 20

但是還有一種位數特別多的數字我們習慣用科學計數法的表示方法來寫:

let billion = 1000000000;
let b = 1e9

以上兩種寫法是一個意思, 1e9 表示 1 x $10^9$;如果是 1e-3 表示 1 / $10^3$ = 0.001。 在 JavaScript 中也可以用數字表示不同的進制,比如:十進制中的 10 在 二、八和十六進制中可以分別表示成 0b10100o120xa;其中的 0b 是二進制前綴, 0o 是八進制前綴,而 ox 是十六進制的前綴。

我們也可以通過 toString(base) 方法來進行進制之間的轉換, base 是進制的基數,表示幾進制,默認是 10 進制的,會返回一個轉換數值的字符串表示。比如:

let num = 10
console.log( num.toString( 2 ))  // '1010'
console.log( num.toString( 8 ))  // '12'
console.log( num.toString( 16 ))  // 'a'

數字也可以直接調用方法, 10..toString(2) 這里的 2個 . 號不是寫錯了,而是必須是2個,否則會報 SyntaxError 錯誤。第一個點表示小數點,第二個才是調用方法。點符號首先會被認為是數字常量的一部分,其次再被認為是屬性訪問符,如果只寫一個點的話,計算機無法知道這個是表示一個小數呢還是去調用函數。數字直接調用函數還可以有以下幾種寫法:

(10).toString(2)  // 將10用括號包起來
10.0.toString(2)  // 將10寫成10.0的形式
10 .toString(2)   // 空格加上點符號調用

Number 類型除了常規(guī)數字之外,還包含了一些特殊的數字:

  • NaN:表示不是一個數字,通常是由不合理的計算導致的結果,比如數字除以字符串 1/'a'; NaN 和任何數進行比較都是返回 false,包括他自己: NaN==NaN 會返回 false; 如何判斷一個數是不是 NaN 呢?有四種方法:

方法一:通過 isNaN() 函數,這個方法會對傳入的字符串也返回 true,所以判斷不準確,不推薦使用:

isNaN( 1 / 'a')`  // true
isNaN( 'a' )  // true

方法二:通過 Number.isNaN(),推薦使用:

Number.isNaN( 1 / 'a')`  // true
Number.isNaN( 'a' )  // false

方法三:通過 Object.is(a,isNaN):

Object.is( 0/'a', NaN) // true
Object.is( 'a', NaN) // false

方法四:通過判斷 n!==n,返回 true, 則 nNaN :

let s = 1/'a'
console.log( s !== s )  // true

  • +Infinity:表示正無窮大,比如 1/0 計算的結果, -Infinity 表示負無窮大,比如 -1/0 的結果。
  • +0-0JavaScript 中的數字都有正負之分,包括零也是這樣,他們會絕對相等:

console.log( +0 === -0 )  // true

為什么 0.1 + 0.2 不等于 0.3

console.log( 0.1 + 0.2 == 0.3 )  // false

有沒有想過為什么上面的會不相等?因為數字在 JavaScript 內部是用二進制進行存儲的,其遵循 IEEE754 標準的,用 64 位來存儲一個數字, 64 位又被分隔成 1、 1152 位來分別表示符號位、指數位和尾數位。

為什么 0.1 + 0.2 不等于 0.3

比如十進制的 0.1 轉成二進制后是多少?我們手動計算一下,十進制小數轉二進制小數的規(guī)則是“乘2取整,順序排列”,具體做法是:用2乘十進制小數,可以得到積,將積的整數部分取出,再用2乘余下的小數 部分,又得到一個積,再將積的整數部分取出,如此進行,直到積中的小數部分為零,或者達到所要求的精度為止。

0.1 * 2 = 0.2  // 第1步:整數為0,小數0.2
0.2 * 2 = 0.4  // 第2步:整數為0,小數0.4
0.4 * 2 = 0.8  // 第3步:整數為0,小數0.8
0.8 * 2 = 1.6  // 第4步:整數為1,小數0.6
0.6 * 2 = 1.2  // 第5步:整數為1,小數0.2
0.2 * 2 = 0.4  // 第6步:整數為0,小數0.4
0.4 * 2 = 0.8  // 第7步:整數為0,小數0.8...

我們這樣依次計算下去之后發(fā)現得到整數的順序排列是 0001100110011001100.... 無限循環(huán),所以理論上十進制的 0.1 轉成二進制后會是一個無限小數 0.0001100110011001100...,用科學計數法表示后將是 1.100110011001100... x $2^{-4}$ ,但是由于 IEEE754 標準規(guī)定了一個數字的存儲位數只能是 64 位,有效位數是 52 位,所以將會對 1100110011001100.... 這個無限數字進行舍入總共 52 位作為有效位,然后二進制的末尾取舍規(guī)則是看后一位數如果是 1 則進位,如果是 0 則直接舍去。那么由于 1100110011001100.... 這串數字的第 53 位剛好是 1 ,所以最終的會得到的數字是 1100110011001100110011001100110011001100110011001101,即 1.100110011001100110011001100110011001100110011001101 x $2^{-4}$。 十進制轉二進制也可以用 toString 來進行轉化:

console.log( 0.1.toString(2) )  // '0.0001100110011001100110011001100110011001100110011001101'

我們發(fā)現十進制的 0.1 在轉化成二進制小數的時候發(fā)生了精度的丟失,由于進位,它比真實的值更大了。而 0.2 其實也有這樣的問題,也會發(fā)生精度的丟失,所以實際上 0.1+0.2 不會等于 0.3:

console.log( 0.1 + 0.2 )  // 0.30000000000000004

那是不是沒辦法判斷兩個小數是否相等了呢?答案肯定是否定的,想要判斷2個小數 n1n2 是否相等可以如下操作:

  • 方法一:兩小數之差的絕對值如果比 Number.EPSILON 還小,那么說明兩數是相等的。

Number.EPSILONES6 中的誤差精度,實際值可以認為等于 $2^{-52}$。

if ( Math.abs( n1 - n2 ) < Number.EPSILON ) {
    console.log( 'n1 和 n2 相等' )
}

  • 方法二:通過 toFixed(n) 對結果進行舍入, toFixed() 將會返回字符串,我們可以用 一元加 + 將其轉成數字:

let sum = 0.1 + 0.2
console.log( +sum.toFixed(2) === 0.3 )  // true

數值的轉化

對數字進行操作的時候將常常遇到數值的舍入和字符串轉數字的問題,這里我們鞏固下基礎。先來看舍入的:

  • Math.floor(),向下舍入,得到一個整數:

Math.floor(2.2)  // 2
Math.floor(2.8)  // 2

  • Math.ceil(),向上舍入,得到一個整數:

Math.ceil(2.2)  // 3
Math.ceil(2.8)  // 3

  • Math.round(),對第一位小數進行四舍五入:

Math.round(2.26)  // 2
Math.round(2.46)  // 2
Math.round(2.50)  // 3

  • Number.prototype.toFixed(n),和 Math.round() 一樣會進行四舍五入,將數字舍入到小數點后 n 位,并且以字符串的形式返回:

12..toFixed(2)  // '12.00'
12.14.toFixed(1)  // '12.1'
12.15.toFixed(1)  // '12.2'

為什么 6.35.toFixed(1) 會等于 6.3 ?因為 6.35 其實是一個無限小數:

6.35.toFixed(20)  // "6.34999999999999964473"

所以在 6.35.toFixed(1) 求值的時候會得到 6.3。

再來看看字符串轉數字的情況:

  • Number(n)+n,直接將 n 進行嚴格轉化:

Number(' ')  // 0
console.log( +'') // 0
Number('010')  // 10
console.log( +'010' )  // 10
Number('12a')  // NaN
console.log( +'12a' )  // NaN

  • parseInt(),非嚴格轉化,從左到右解析字符串,遇到非數字就停止解析,并且把解析的數字返回:

parseInt('12a')  // 12
parseInt('a12')  // NaN
parseInt('')  // NaN
parseInt('0xA')  // 10,0x開頭的將會被當成十六進制數

parseInt() 默認是用十進制去解析字符串的,其實他是支持傳入第二個參數的,表示要以多少進制的 基數去解析第一個參數:

parseInt('1010', 2)  // 10
parseInt('ff', 16)  // 255

如何判斷一個數是不是整數?介紹兩種方法:

  • 方法一:通過 Number.isInteger():

Number.isInteger(12.0)  // true
Number.isInteger(12.2)  // false

  • 方法二: typeofnum=='number'&&num%1==0

function isInteger(num) {
    return typeof num == 'number' && num % 1 == 0
}

引用類型

除了原始類型外,還有一個特別重要的類型:引用類型。高程里這樣描述他:引用類型是一種數據結構, 用于將數據和功能組織在一起。到目前為止,我們看到最多的引用類型就是 Object,創(chuàng)建一個 Object 有兩種方式:

  • 方式一:通過 new 操作符:

let person = new Object()
person.name = 'bubuzou'
person.age = 20

  • 方式二:通過對象字面量,這是我們最喜歡用的方式:

let person = {
    name: 'bubuzou',
    age: 20
}

內置的引用類型

除了 Object 外,在 JavaScript 中還有別的內置的引用類型,比如:

  • Array 數組
  • Date 日期
  • RegExp 正則表達式
  • Function 函數

他們的原型鏈的頂端都會指向 Object:

let d = new Date()
console.log( d.__proto__.__proto__.constructor )  // ? Object() { [native code] }

包裝類型

先來看一個問題,為什么原始類型的變量沒有屬性和方法,但是卻能夠調用方法呢?

let str = 'bubuzou'
str.substring(0, 3)  // 'bub'

因為 JavaScript 為了更好地操作原始類型,設計出了幾個對應的包裝類型,他們分別是:

  • Boolean
  • Number
  • String

上面那串代碼的執(zhí)行過程其實是這樣的:

  1. 創(chuàng)建 String 類型的一個實例;
  2. 在實例上調用指定的方法;
  3. 銷毀這個實例

用代碼體現一下:

let str = new
String('bubuzou')
str.substring(0, 3)
str = null

原始類型調用函數其實就是自動進行了裝箱操作,將原始類型轉成了包裝類型,然后其實原始類型和包裝類型是有本質區(qū)別的,原始類型是原始值,而包裝類型是對象實例:

let str1 = 'bubuzou'
let str2 = new String('bubuzou')
console.log( str1 === str2 )  // fasle
console.log( typeof str1 )  // 'string'
console.log( typeof str2 )  // 'object'

居然有裝箱操作,那肯定也有拆箱操作,所謂的拆箱就是包裝類型轉成原始類型的過程,又叫 ToPromitive,來看下面的例子:

let obj = {
    toString: () => { return 'bubuzou' },    
    valueOf: () => { return 20 },
}
console.log( +obj )  // 20
console.log( `${obj}` )  // 'bubuzou'

在拆箱操作的時候,默認會嘗試調用包裝類型的 toString()valueOf() 方法,對于不同的 hint 調用順序會有所區(qū)別,如果 hintstring 則優(yōu)先調用 toString(),否則的話,則優(yōu)先調用 valueOf()。 默認情況下,一個 Object 對象具有 toString()valueOf() 方法:

let obj = {}
console.log( obj.toString() )  // '[object Object]'
console.log( obj.valueOf() )  // {},valueOf會返回對象本身

類型裝換

Javascript 是弱類型的語音,所以對變量進行操作的時候經常會發(fā)生類型的轉換,尤其是隱式類型轉換,可能會讓代碼執(zhí)行結果出乎意料之外,比如如下的代碼你能理解其執(zhí)行結果嘛?

[] + {}  // '[object Object]'
{} + []  // 0

類型轉換規(guī)則

所以我們需要知道類型轉換的規(guī)則,以下整理出一個表格,列出了常見值和類型以及轉換之后的結果,僅供參考。

類型轉換規(guī)則

顯示類型轉換

我們平時寫代碼的時候應該盡量讓寫出來的代碼通俗易懂,讓別人能閱讀后知道你是要做什么,所以在對類型進行判斷的時候應該盡量顯示的處理。 比如將字符串轉成數字,可以這樣:

Number( '21' )  // 21
Number( '21.8' )  // 21.8
+'21'  // 21 

將數字顯示轉成字符串可以這樣:

String(21)  // '21'
21..toString()  // '21'

顯示轉成布爾類型可以這樣:

Boolean('21')  // true
Boolean( undefined )  // false
!!NaN  // false
!!'21'  // true

除了以上之外,還有一些關于類型轉換的冷門操作,有時候也挺管用的: 直接用一元加操作符獲取當前時間的毫秒數:

+new Date()  // 1595517982686

~ 配合 indexOf() 將操作結果直接轉成布爾類型:

let str = 'bubuzou.com'
if (~str.indexOf('.com')) {
    console.log( 'str如果包含了.com字符串,則會打印這句話' )
}

使用 ~~ 對字符或數字截取整數,和 Math.floor() 有稍許不同:

~~21.1  // 21
~~-21.9  // -21
~~'1.2a'  // 0
Math.floor( 21.1 )  // 21
Math.floor( -21.9 )  // -22

隱式類型轉換

隱式類型轉換發(fā)生在 JavaScript 的運行時,通常是由某些操作符或語句引起的,有下面這幾種情況:

  • 隱式轉成布爾類型:

  1. if(..)語句中的條件判斷表達式。
  2. for(..;..;..)語句中的條件判斷表達式(第二個)。
  3. while(..)do..while(..) 循環(huán)中的條件判斷表達式。
  4. ?:中的條件判斷表達式。
  5. 邏輯運算符 || (邏輯或)和 && (邏輯與)左邊的操作數(作為條件判斷表達式)

if (42) {
    console.log(42)
}
while ('bubuzou') {
    console.log('bubuzou')
}
const c = null ? '存在' : '不存在'  // '不存在'

上例中的非布爾值會被隱式強制類型轉換為布爾值以便執(zhí)行條件判斷。 需要特別注意的是 ||&& 操作符。 || 的操作過程是只有當左邊的值返回 false 的時候才會對右邊進行求值且將它作為最后結果返回,類似 a?a:b 這種效果:

const a = 'a' || 'b'  // 'a'
const b = '' || 'c'  // 'c'

&& 的操作過程是只有當左邊的值返回 true 的時候才對右邊進行求值且將右邊的值作為結果返回,類似 a?b:a 這種效果:

const a = 'a' && 'b'  // 'b'
const b = '' && 'c'  // ''

  • 數學操作符 -*/ 會對非數字類型的會優(yōu)先轉成數字類型,但是對 + 操作符會比較特殊:

  1. 當一側為 String 類型,被識別為字符串拼接,并會優(yōu)先將另一側轉換為字符串類型。
  2. 當一側為 Number 類型,另一側為原始類型,則將原始類型轉換為 Number 類型。
  3. 當一側為 Number 類型,另一側為引用類型,將引用類型和 Number 類型轉換成字符串后拼接。

42 + 'bubuzou'  // '42bubuzou'
42 + null  // 42
42 + true  // 43
42 + []  // '42'
42 + {}  // '42[object Object]'

  • 寬松相等和嚴格相等

寬松相等( ==)和嚴格相等( ===)在面試的時候經常會被問到,而回答一般是 == 是判斷值是否相等,而 === 除了判斷值會不會相等之外還會判斷類型是否相等,這個答案不完全正確,更好的回答是: == 在比較過程中允許發(fā)生隱式類型轉換,而 === 不會。 那 == 是怎么進行類型轉換的呢?

1、 數字和字符串比,字符串將轉成數字進行比較:

20 == '20'  // true
20 === '20'  // false

2、 別的類型和布爾類型比較,布爾類型將首先轉成數字進行比較, true 轉成數字 1, false 轉成數字 0,注意這個是非常容易出錯的一個點:

'bubuzou' == true  // false
'0' == false  // true
null == false  // false,
undefined == false  // false
[] == true  // false
['1']  == true  // true

所以寫代碼進行判斷的時候一定不要寫成 x==truex==false 這種,而應該直接 if(x) 判斷。

3、 nullundefined: null==undefined 比較結果是 true,除此之外, nullundefined 和其他任何結果的比較值都為 false。可以認為在 == 的情況下, nullundefined 可以相互的進行隱式類型轉換。

null == undefined // true
null == '' // false
null == 0 // false
null == false // false
undefined == '' // false
undefined == 0 // false
undefined == false // false

4、 原始類型和引用類型比較,引用類型會首先進行 ToPromitive 轉成原始類型然后進行比較,規(guī)則參考上面介紹的拆箱操作:

'42'  == [42]  // true
'1,2,3'  == [1, 2, 3]  // true
'[object Object]' == {}  // true
0 == [undefined]  // true

5、 特殊的值

NaN == NaN  // false
+0 == -0  // true
[] == ![]  // true,![]的優(yōu)先級比==高,所以![]先轉成布爾值變成false;即變成[] == false,false再轉成數字0,[]轉成數字0,所以[] == ![]
0 == '\n'  // true

類型檢測

用typeof檢測原始類型

JavaScript 中有 null、 undefined、 boolean、 number、 string、 Symbol 等六種原始類型,我們可以用 typeof 來判斷值是什么原始類型的,會返回類型的字符串表示:

typeof undefined // 'undefined'
typeof true  // 'boolean'
typeof 42  // 'number'
typeof "42"  // 'string'
typeof Symbol()  // 'symbol'

但是原始類型中有一個例外, typeofnull 會得到 'object',所以我們用 typeof 對原始值進行類型判斷的時候不能得到一個準確的答案,那如何判斷一個值是不是 null 類型的呢?

let o = null
!o && typeof o === 'object' // 用于判斷 o 是否是 null 類型

undefinedundeclared 有什么區(qū)別?前者是表示在作用域中定義了但是沒有賦值的變量,而后者是表示在作用域中沒有定義的變量;分別表示 undefined 未定義、 undeclared 未聲明。

typeof 能夠對原始類型進行判斷,那是否也能判斷引用類型呢?

typeof []  // 'object'
typeof {}  // 'object'
typeof new Date()  // 'object'
typeof new RegExp()  // 'object'
typeof new Function()  // 'function'

從上面的結果我們可以得到這樣一個結論: typeof 對引用類型判斷的時候只有 function 類型可以正確判斷,其他都無法正確判斷具體是什么引用類型。

用instanceof檢測引用類型

我們知道 typeof 只能對部分原始類型進行檢測,對引用類型毫無辦法。 JavaScript 提供了一個操作符 instanceof,我們來看下他是否能檢測引用類型:

[] instanceof Array  // true
[] instanceof Object  // true 

我們發(fā)現數組即是 Array 的實例,也是 Object 的實例,因為所以引用類型原型鏈的終點都是 Object,所以 Array 自然是 Object 的實例。那么我們得出結論: instanceof 用于檢測引用類型好像也不是很靠譜的選擇。

用toString進行類型檢測

我們可以使用 Object.prototype.toString.call() 來檢測任何變量值的類型:

Object.prototype.toString.call(true)  // '[object Boolean]'
Object.prototype.toString.call(undefined)  // '[object Undefined]'
Object.prototype.toString.call(null)  // '[object Null]'
Object.prototype.toString.call(20)  // '[object Number]'
Object.prototype.toString.call('bubuzou')  // '[object String]'
Object.prototype.toString.call(Symbol())  // '[object Symbol]'
Object.prototype.toString.call([])  // '[object Array]'
Object.prototype.toString.call({})  // '[object Object]'
Object.prototype.toString.call(function(){})  // '[object Function]'
Object.prototype.toString.call(new Date())  // '[object Date]'
Object.prototype.toString.call(new RegExp())  // '[object RegExp]'
Object.prototype.toString.call(JSON)  // '[object JSON]'
Object.prototype.toString.call(MATH)  // '[object MATH]'
Object.prototype.toString.call(window)  // '[object RegExp]'

文章來源于公眾號:大海我來了 ,作者布蘭

以上就是W3Cschool編程獅關于初中級前端必須要知道的JS數據類型的相關介紹了,希望對大家有所幫助。

0 人點贊