App下載

初中級(jí)前端必須要知道的JS數(shù)據(jù)類型

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

JavaScript中有哪些數(shù)據(jù)類型?

計(jì)算機(jī)世界中定義的數(shù)據(jù)類型其實(shí)就是為了描述現(xiàn)實(shí)世界中存在的事實(shí)而定義的。比如我們用人來(lái)舉例:

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

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

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

原始類型的不可變性

在回答這個(gè)問題之前,我們先看一下變量在內(nèi)存中是如何存儲(chǔ)的:

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

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

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

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

棧內(nèi)存:

  • 順序存儲(chǔ)結(jié)構(gòu),特點(diǎn)是先進(jìn)后出。就像一個(gè)兵乒球盒子一樣,兵乒球從外面一個(gè)個(gè)的放入盒子里,最先取出來(lái)的一定是最后放入盒子的那個(gè)。
  • 存儲(chǔ)空間固定
  • 可以直接操作其保存的值,執(zhí)行效率高

堆內(nèi)存:

  • 無(wú)序的存儲(chǔ)結(jié)構(gòu)
  • 存儲(chǔ)空間可以動(dòng)態(tài)變化
  • 無(wú)法直接操作其內(nèi)部的存儲(chǔ),需要通過引用地址操作

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

變量的存儲(chǔ)結(jié)構(gòu)

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

從而我們得出結(jié)論:原始類型的值都是固定的,而對(duì)象類型則是由原始類型的鍵值對(duì)組合成一個(gè)復(fù)雜的對(duì)象;他們?cè)趦?nèi)存中的存儲(chǔ)方式是不一樣的,原始類型的值直接存在棧內(nèi)存中,而對(duì)象類型的實(shí)際值是存在堆內(nèi)存中的,在棧內(nèi)存中保存了一份引用地址,這個(gè)地址指向堆內(nèi)存中的實(shí)際值,所以對(duì)象類型又習(xí)慣被叫做引用類型。

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

比較

當(dāng)我們對(duì)兩個(gè)變量進(jìn)行比較的時(shí)候,不同類型的變量是有不同表現(xiàn)的:

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個(gè)字符串變量和2個(gè)對(duì)象變量,他們都長(zhǎng)一模一樣,但是字符串變量會(huì)相等,對(duì)象變量卻不相等。這是因?yàn)樵?JavaScript 中,原型類型進(jìn)行比較的時(shí)候比較的是存在棧中的值是否相等;而引用類型進(jìn)行比較的時(shí)候,是比較棧內(nèi)存中的引用地址是否相等。 如上幾個(gè)變量在內(nèi)存中的存儲(chǔ)模型如圖所示:

棧內(nèi)存

復(fù)制

變量進(jìn)行復(fù)制的時(shí)候,原始類型和引用類型變量也是有區(qū)別的,來(lái)看下面的代碼:

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

復(fù)制

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

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

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

引用類型的復(fù)制

原始類型進(jìn)行復(fù)制的時(shí)候是變量的值進(jìn)行重新賦值,而如上圖所示:引用類型進(jìn)行復(fù)制的時(shí)候是把變量所指向的引用地址進(jìn)行賦值給新的變量,所以復(fù)制后 person1person2 都指向堆內(nèi)存中的同一個(gè)值,所以當(dāng)改變 person2.name 的時(shí)候, person1.name 也會(huì)被改變就是這個(gè)原因。

值傳遞和引用傳遞

先說(shuō)一下結(jié)論,在 JavaScript 中,所有函數(shù)的參數(shù)傳遞都是按值進(jìn)行傳遞的。看如下代碼:

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

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

再來(lái)看看引用類型的傳參,會(huì)不會(huì)有所不同呢?

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

引用類型進(jìn)行函數(shù)傳參的時(shí)候,會(huì)把引用地址復(fù)制給局部變量,所以全局的 person 和函數(shù)內(nèi)部的局部變量 person 是指向同一個(gè)堆地址的,所以一旦一方改變,另一方也將被改變,所以至此我們是不是可以下結(jié)論說(shuō):當(dāng)函數(shù)進(jìn)行傳參的時(shí)候如果參數(shù)是引用類型那么就是引用傳遞嘛?

將上面的例子改造下:

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

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

null 和 undefined 傻傻分不清?

nullJavaScript 中自成一種原始類型,只有一個(gè)值 null,表示無(wú)、空、值未知等特殊值。可以直接給一個(gè)變量賦值為 null

let s = null

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

let s
console.log( s)  // undefined

雖然可以給變量直接賦值為 undefined 也不會(huì)報(bào)錯(cuò),但是原則上如果一個(gè)變量值未定,或者表示空,則直接賦值為 null 比較合適,不建議給變量賦值 undefined。 nullundefined 在進(jìn)行邏輯判斷的時(shí)候都是會(huì)返回 false 的:

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

null 在轉(zhuǎn)成數(shù)字類型的時(shí)候會(huì)變成 0,而 undefined 會(huì)變成 NaN:

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

認(rèn)識(shí)新的原始類型 Symbol

Symbol 值表示唯一標(biāo)識(shí)符,是 ES6 中新引進(jìn)的一種原始類型??梢酝ㄟ^ Symbol() 來(lái)創(chuàng)建一個(gè)重要的值,也可以傳入描述值;其唯一性體現(xiàn)在即使是傳入一樣的描述,他們兩者之間也是不會(huì)相等的:

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

全局的 Symbol

那還是不是任意2個(gè)描述一樣的 Symbol 都是不相等的呢?答案是否定的。可以通過 Symbol.for() 來(lái)查找或新建一個(gè) Symbol

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

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

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

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

但是對(duì)于任何一個(gè) Symbol 都有一個(gè)屬性 description,表示這個(gè) Symbol 的描述:

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

Symbol 作為對(duì)象屬性

我們知道對(duì)象的屬性鍵可以是字符串,但是不能是 Number 或者 BooleanSymbol 被設(shè)計(jì)出來(lái)其實(shí)最大的初衷就是用于對(duì)象的屬性鍵:

let age = Symbol('20')
let person = {
    name: 'bubuzou', 
    [age]: '20',  // 在對(duì)象字面量中使用 `Symbol` 的時(shí)候需要使用中括號(hào)包起來(lái)
}

這里給 person 定義了一個(gè) Symbol 作為屬性鍵的屬性,這個(gè)相比于用字符串作為屬性鍵有啥好處呢?最明顯的好處就是如果這個(gè) person 對(duì)象是多個(gè)開發(fā)者進(jìn)行開發(fā)維護(hù),那么很容易再給 person 添加屬性的時(shí)候出現(xiàn)同名的,如果是用字符串作為屬性鍵那肯定是沖突了,但是如果用 Symbol 作為屬性鍵,就不會(huì)存在這個(gè)問題了,因?yàn)樗俏ㄒ粯?biāo)識(shí)符,所以可以使對(duì)象的屬性受到保護(hù),不會(huì)被意外的訪問或者重寫。

注意一點(diǎn),如果用 Symbol 作為對(duì)象的屬性鍵的時(shí)候, forin 、 Object.getOwnPropertyNames、或 Object.keys() 這里循環(huán)是無(wú)法獲取 Symbol 屬性鍵的,但是可以通過 Object.getOwnPropertySymbols() 來(lái)獲取;在上面的代碼基礎(chǔ)上:

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 中的數(shù)字涉及到了兩種類型:一種是 Number 類型,以 64 位的格式 IEEE-754 存儲(chǔ),也被稱為雙精度浮點(diǎn)數(shù),就是我們平常使用的數(shù)字,其范圍是 $2^{52}$ 到 -$2^{52}$;第二種類型是 BigInt,能夠表示任意長(zhǎng)度的整數(shù),包括超出 $2^{52}$ 到 -$2^{52}$ 這個(gè)范圍外的數(shù)。這里我們只介紹 Number 數(shù)字。

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

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

let age = 20

但是還有一種位數(shù)特別多的數(shù)字我們習(xí)慣用科學(xué)計(jì)數(shù)法的表示方法來(lái)寫:

let billion = 1000000000;
let b = 1e9

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

我們也可以通過 toString(base) 方法來(lái)進(jìn)行進(jìn)制之間的轉(zhuǎn)換, base 是進(jìn)制的基數(shù),表示幾進(jìn)制,默認(rèn)是 10 進(jìn)制的,會(huì)返回一個(gè)轉(zhuǎn)換數(shù)值的字符串表示。比如:

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

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

(10).toString(2)  // 將10用括號(hào)包起來(lái)
10.0.toString(2)  // 將10寫成10.0的形式
10 .toString(2)   // 空格加上點(diǎn)符號(hào)調(diào)用

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

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

方法一:通過 isNaN() 函數(shù),這個(gè)方法會(huì)對(duì)傳入的字符串也返回 true,所以判斷不準(zhǔn)確,不推薦使用:

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:表示正無(wú)窮大,比如 1/0 計(jì)算的結(jié)果, -Infinity 表示負(fù)無(wú)窮大,比如 -1/0 的結(jié)果。
  • +0-0, JavaScript 中的數(shù)字都有正負(fù)之分,包括零也是這樣,他們會(huì)絕對(duì)相等:

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

為什么 0.1 + 0.2 不等于 0.3

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

有沒有想過為什么上面的會(huì)不相等?因?yàn)閿?shù)字在 JavaScript 內(nèi)部是用二進(jìn)制進(jìn)行存儲(chǔ)的,其遵循 IEEE754 標(biāo)準(zhǔn)的,用 64 位來(lái)存儲(chǔ)一個(gè)數(shù)字, 64 位又被分隔成 1、 1152 位來(lái)分別表示符號(hào)位、指數(shù)位和尾數(shù)位。

為什么 0.1 + 0.2 不等于 0.3

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

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

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

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

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

console.log( 0.1 + 0.2 )  // 0.30000000000000004

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

  • 方法一:兩小數(shù)之差的絕對(duì)值如果比 Number.EPSILON 還小,那么說(shuō)明兩數(shù)是相等的。

Number.EPSILONES6 中的誤差精度,實(shí)際值可以認(rèn)為等于 $2^{-52}$。

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

  • 方法二:通過 toFixed(n) 對(duì)結(jié)果進(jìn)行舍入, toFixed() 將會(huì)返回字符串,我們可以用 一元加 + 將其轉(zhuǎn)成數(shù)字:

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

數(shù)值的轉(zhuǎn)化

對(duì)數(shù)字進(jìn)行操作的時(shí)候?qū)⒊3S龅綌?shù)值的舍入和字符串轉(zhuǎn)數(shù)字的問題,這里我們鞏固下基礎(chǔ)。先來(lái)看舍入的:

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

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

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

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

  • Math.round(),對(duì)第一位小數(shù)進(jìn)行四舍五入:

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

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

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

為什么 6.35.toFixed(1) 會(huì)等于 6.3 ?因?yàn)?6.35 其實(shí)是一個(gè)無(wú)限小數(shù):

6.35.toFixed(20)  // "6.34999999999999964473"

所以在 6.35.toFixed(1) 求值的時(shí)候會(huì)得到 6.3

再來(lái)看看字符串轉(zhuǎn)數(shù)字的情況:

  • Number(n)+n,直接將 n 進(jìn)行嚴(yán)格轉(zhuǎn)化:

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

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

parseInt('12a')  // 12
parseInt('a12')  // NaN
parseInt('')  // NaN
parseInt('0xA')  // 10,0x開頭的將會(huì)被當(dāng)成十六進(jìn)制數(shù)

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

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

如何判斷一個(gè)數(shù)是不是整數(shù)?介紹兩種方法:

  • 方法一:通過 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
}

引用類型

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

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

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

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

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

內(nèi)置的引用類型

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

  • Array 數(shù)組
  • Date 日期
  • RegExp 正則表達(dá)式
  • Function 函數(shù)

他們的原型鏈的頂端都會(huì)指向 Object:

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

包裝類型

先來(lái)看一個(gè)問題,為什么原始類型的變量沒有屬性和方法,但是卻能夠調(diào)用方法呢?

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

因?yàn)?JavaScript 為了更好地操作原始類型,設(shè)計(jì)出了幾個(gè)對(duì)應(yīng)的包裝類型,他們分別是:

  • Boolean
  • Number
  • String

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

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

用代碼體現(xiàn)一下:

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

原始類型調(diào)用函數(shù)其實(shí)就是自動(dòng)進(jìn)行了裝箱操作,將原始類型轉(zhuǎn)成了包裝類型,然后其實(shí)原始類型和包裝類型是有本質(zhì)區(qū)別的,原始類型是原始值,而包裝類型是對(duì)象實(shí)例:

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

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

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

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

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

類型裝換

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

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

類型轉(zhuǎn)換規(guī)則

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

類型轉(zhuǎn)換規(guī)則

顯示類型轉(zhuǎn)換

我們平時(shí)寫代碼的時(shí)候應(yīng)該盡量讓寫出來(lái)的代碼通俗易懂,讓別人能閱讀后知道你是要做什么,所以在對(duì)類型進(jìn)行判斷的時(shí)候應(yīng)該盡量顯示的處理。 比如將字符串轉(zhuǎn)成數(shù)字,可以這樣:

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

將數(shù)字顯示轉(zhuǎn)成字符串可以這樣:

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

顯示轉(zhuǎn)成布爾類型可以這樣:

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

除了以上之外,還有一些關(guān)于類型轉(zhuǎn)換的冷門操作,有時(shí)候也挺管用的: 直接用一元加操作符獲取當(dāng)前時(shí)間的毫秒數(shù):

+new Date()  // 1595517982686

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

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

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

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

隱式類型轉(zhuǎn)換

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

  • 隱式轉(zhuǎn)成布爾類型:

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

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

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

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

&& 的操作過程是只有當(dāng)左邊的值返回 true 的時(shí)候才對(duì)右邊進(jìn)行求值且將右邊的值作為結(jié)果返回,類似 a?b:a 這種效果:

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

  • 數(shù)學(xué)操作符 -*/ 會(huì)對(duì)非數(shù)字類型的會(huì)優(yōu)先轉(zhuǎn)成數(shù)字類型,但是對(duì) + 操作符會(huì)比較特殊:

  1. 當(dāng)一側(cè)為 String 類型,被識(shí)別為字符串拼接,并會(huì)優(yōu)先將另一側(cè)轉(zhuǎn)換為字符串類型。
  2. 當(dāng)一側(cè)為 Number 類型,另一側(cè)為原始類型,則將原始類型轉(zhuǎn)換為 Number 類型。
  3. 當(dāng)一側(cè)為 Number 類型,另一側(cè)為引用類型,將引用類型和 Number 類型轉(zhuǎn)換成字符串后拼接。

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

  • 寬松相等和嚴(yán)格相等

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

1、 數(shù)字和字符串比,字符串將轉(zhuǎn)成數(shù)字進(jìn)行比較:

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

2、 別的類型和布爾類型比較,布爾類型將首先轉(zhuǎn)成數(shù)字進(jìn)行比較, true 轉(zhuǎn)成數(shù)字 1, false 轉(zhuǎn)成數(shù)字 0,注意這個(gè)是非常容易出錯(cuò)的一個(gè)點(diǎn):

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

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

3、 nullundefined: null==undefined 比較結(jié)果是 true,除此之外, nullundefined 和其他任何結(jié)果的比較值都為 false??梢哉J(rèn)為在 == 的情況下, nullundefined 可以相互的進(jìn)行隱式類型轉(zhuǎn)換。

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

4、 原始類型和引用類型比較,引用類型會(huì)首先進(jìn)行 ToPromitive 轉(zhuǎn)成原始類型然后進(jìn)行比較,規(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)先級(jí)比==高,所以![]先轉(zhuǎn)成布爾值變成false;即變成[] == false,false再轉(zhuǎn)成數(shù)字0,[]轉(zhuǎn)成數(shù)字0,所以[] == ![]
0 == '\n'  // true

類型檢測(cè)

用typeof檢測(cè)原始類型

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

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

但是原始類型中有一個(gè)例外, typeofnull 會(huì)得到 'object',所以我們用 typeof 對(duì)原始值進(jìn)行類型判斷的時(shí)候不能得到一個(gè)準(zhǔn)確的答案,那如何判斷一個(gè)值是不是 null 類型的呢?

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

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

typeof 能夠?qū)υ碱愋瓦M(jìn)行判斷,那是否也能判斷引用類型呢?

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

從上面的結(jié)果我們可以得到這樣一個(gè)結(jié)論: typeof 對(duì)引用類型判斷的時(shí)候只有 function 類型可以正確判斷,其他都無(wú)法正確判斷具體是什么引用類型。

用instanceof檢測(cè)引用類型

我們知道 typeof 只能對(duì)部分原始類型進(jìn)行檢測(cè),對(duì)引用類型毫無(wú)辦法。 JavaScript 提供了一個(gè)操作符 instanceof,我們來(lái)看下他是否能檢測(cè)引用類型:

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

我們發(fā)現(xiàn)數(shù)組即是 Array 的實(shí)例,也是 Object 的實(shí)例,因?yàn)樗砸妙愋驮玩湹慕K點(diǎn)都是 Object,所以 Array 自然是 Object 的實(shí)例。那么我們得出結(jié)論: instanceof 用于檢測(cè)引用類型好像也不是很靠譜的選擇。

用toString進(jìn)行類型檢測(cè)

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

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]'

文章來(lái)源于公眾號(hào):大海我來(lái)了 ,作者布蘭

以上就是W3Cschool編程獅關(guān)于初中級(jí)前端必須要知道的JS數(shù)據(jù)類型的相關(guān)介紹了,希望對(duì)大家有所幫助。

0 人點(diǎn)贊