JavaScript中有哪些數(shù)據(jù)類型?
計(jì)算機(jī)世界中定義的數(shù)據(jù)類型其實(shí)就是為了描述現(xiàn)實(shí)世界中存在的事實(shí)而定義的。比如我們用人來舉例:
- 有沒有人在房間里?這里的有和沒有就是是或者非的概念,在
JS
中對(duì)應(yīng)Boolean
類型,true
表示是,false
表示非; - 有幾個(gè)人在房間里?這里的幾個(gè)表示的是一個(gè)量級(jí)概念,在
JS
中對(duì)應(yīng)Number
類型,包含整數(shù)和浮點(diǎn)數(shù),還有一些特殊的值,比如:-Infinity
表示負(fù)無窮大、+Infinity
表示正無窮大、NaN
表示不是一個(gè)數(shù)字; - 房間里的這些人都是我的朋友。這是一句陳述語句,這種文本類的信息將會(huì)以字符串形式進(jìn)行存儲(chǔ),在
JS
中對(duì)應(yīng)String
類型; - 房間里沒有人。這里的沒有代表無和空的概念,在
JS
中null
和undefined
都可以表示這個(gè)意思; - 現(xiàn)實(shí)世界中所有人都是獨(dú)一無二的,這在
JS
中對(duì)應(yīng)Symbol
類型,表示唯一且不可改變; Number
所表示的整數(shù)是有范圍的,超出范圍的數(shù)據(jù)就沒法用Number
表示了,于是ES10
中提出了一種新的數(shù)據(jù)類型BigInt
,能表示任何位數(shù)的整數(shù);- 以上提到的
Boolean
、Number
、String
、null
、undefined
、Symbol
和BigInt
等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
。這就說明了字符串的不可變性。但是你看了下面的這段代碼,你就會(huì)產(chǎn)生疑問了:
let name1 = 'bubuzou'
name1 += '.com'
console.log(name1) // 'bubuzou.com'
你說字符串是不可變的,那現(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è)的放入盒子里,最先取出來的一定是最后放入盒子的那個(gè)。
- 存儲(chǔ)空間固定
- 可以直接操作其保存的值,執(zhí)行效率高
堆內(nèi)存:
- 無序的存儲(chǔ)結(jié)構(gòu)
- 存儲(chǔ)空間可以動(dòng)態(tài)變化
- 無法直接操作其內(nèi)部的存儲(chǔ),需要通過引用地址操作
了解完變量在內(nèi)存中的存儲(chǔ)方式有2種,那我們繼續(xù)以上面那串代碼為例,畫出變量的存儲(chǔ)結(jié)構(gòu)圖:
然后我們可以描述下當(dāng)計(jì)算機(jī)執(zhí)行這段代碼時(shí)候的發(fā)生了什么?首先定義了一個(gè)變量 name1
并且給其賦值 bubuzou
這個(gè)時(shí)候就會(huì)在內(nè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)橐妙愋痛笮〔还潭ǎ鴹5拇笮∈枪潭ǖ?,堆空間的大小是可以動(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ì)象變量,他們都長一模一樣,但是字符串變量會(huì)相等,對(duì)象變量卻不相等。這是因?yàn)樵?JavaScript
中,原型類型進(jìn)行比較的時(shí)候比較的是存在棧中的值是否相等;而引用類型進(jìn)行比較的時(shí)候,是比較棧內(nèi)存中的引用地址是否相等。
如上幾個(gè)變量在內(nèi)存中的存儲(chǔ)模型如圖所示:
復(fù)制
變量進(jìn)行復(fù)制的時(shí)候,原始類型和引用類型變量也是有區(qū)別的,來看下面的代碼:
let str1 = 'hello'
let str2 = str1
str2 = 'world'
console.log( str1 ) // 'hello'
letstr1='hello'
: 復(fù)制前,定義了一個(gè)變量str1
,并且給其賦值hello
,這個(gè)時(shí)候hello
這個(gè)字符串就會(huì)在棧內(nèi)存中被分配一塊空間進(jìn)行存儲(chǔ),然后變量str1
會(huì)指向這個(gè)內(nèi)存地址;letstr2=str1
:復(fù)制后,把str1
的值賦值給str2
,這個(gè)時(shí)候會(huì)在棧中新開辟一塊空間用來存儲(chǔ)str2
的值;str2='world'
:給str2
賦值了一個(gè)新的字符串world
,那么將新建一塊內(nèi)存用來存儲(chǔ)world
,同時(shí)str2
原來的值hello
的內(nèi)存空間因?yàn)闆]有變量所引用,所以一段時(shí)間后將被當(dāng)成垃圾回收;console.log(str1)
:因?yàn)?str1
和str2
的棧內(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'
原始類型進(jìn)行復(fù)制的時(shí)候是變量的值進(jìn)行重新賦值,而如上圖所示:引用類型進(jìn)行復(fù)制的時(shí)候是把變量所指向的引用地址進(jìn)行賦值給新的變量,所以復(fù)制后 person1
和 person2
都指向堆內(nèi)存中的同一個(gè)值,所以當(dāng)改變 person2.name
的時(shí)候, person1.name
也會(huì)被改變就是這個(gè)原因。
值傳遞和引用傳遞
先說一下結(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)存里新建了一塊空間用來存放局部變量的值,然后又把局部變量的值改成了 bubuzou.com
,這個(gè)時(shí)候其實(shí)內(nèi)存中會(huì)有3塊地址空間分別用來存放全局變量的值 bubuzou
、局部變量原來的值 bubuzou
、和局部變量新的值 bubuzou.com
;一旦函數(shù)調(diào)用結(jié)束,局部變量將被銷毀,一段時(shí)間后由于局部變量新舊值沒有變量引用,那這兩塊空間將被回收釋放;所以這個(gè)時(shí)候全局 name
的值依然是 bubuzou
。
再來看看引用類型的傳參,會(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é)論說:當(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 傻傻分不清?
null
在 JavaScript
中自成一種原始類型,只有一個(gè)值 null
,表示無、空、值未知等特殊值。可以直接給一個(gè)變量賦值為 null
:
let s = null
undefined
和 null
一樣也是自成一種原始類型,表示定義了一個(gè)變量,但是沒有賦值,則這個(gè)變量的值就是 undefined
:
let s
console.log( s) // undefined
雖然可以給變量直接賦值為 undefined
也不會(huì)報(bào)錯(cuò),但是原則上如果一個(gè)變量值未定,或者表示空,則直接賦值為 null
比較合適,不建議給變量賦值 undefined
。 null
和 undefined
在進(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()
來創(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()
來查找或新建一個(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è)描述為 bubuzou
的 Symbol
,所以這里 a
和 b
是會(huì)絕對(duì)相等的。
居然可以通過描述找到 Symbol
, 那是否可以通過 Symbol
來找到描述呢?答案是肯定的,但是必須是全局的 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
或者 Boolean
; Symbol
被設(shè)計(jì)出來其實(shí)最大的初衷就是用于對(duì)象的屬性鍵:
let age = Symbol('20')
let person = {
name: 'bubuzou',
[age]: '20', // 在對(duì)象字面量中使用 `Symbol` 的時(shí)候需要使用中括號(hào)包起來
}
這里給 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)是無法獲取 Symbol
屬性鍵的,但是可以通過 Object.getOwnPropertySymbols()
來獲??;在上面的代碼基礎(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
,能夠表示任意長度的整數(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ù)法的表示方法來寫:
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
、 0o12
和 0xa
;其中的 0b
是二進(jìn)制前綴, 0o
是八進(jìn)制前綴,而 ox
是十六進(jìn)制的前綴。
我們也可以通過 toString(base)
方法來進(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ī)無法知道這個(gè)是表示一個(gè)小數(shù)呢還是去調(diào)用函數(shù)。數(shù)字直接調(diào)用函數(shù)還可以有以下幾種寫法:
(10).toString(2) // 將10用括號(hào)包起來
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
, 則 n
是 NaN
:
let s = 1/'a'
console.log( s !== s ) // true
+Infinity
:表示正無窮大,比如1/0
計(jì)算的結(jié)果,-Infinity
表示負(fù)無窮大,比如-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
位來存儲(chǔ)一個(gè)數(shù)字, 64
位又被分隔成 1
、 11
和 52
位來分別表示符號(hào)位、指數(shù)位和尾數(shù)位。
比如十進(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....
無限循環(huán),所以理論上十進(jìn)制的 0.1
轉(zhuǎn)成二進(jìn)制后會(huì)是一個(gè)無限小數(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è)無限數(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
來進(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ù) n1
和 n2
是否相等可以如下操作:
- 方法一:兩小數(shù)之差的絕對(duì)值如果比
Number.EPSILON
還小,那么說明兩數(shù)是相等的。
Number.EPSILON
是 ES6
中的誤差精度,實(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ǔ)。先來看舍入的:
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è)無限小數(shù):
6.35.toFixed(20) // "6.34999999999999964473"
所以在 6.35.toFixed(1)
求值的時(shí)候會(huì)得到 6.3
。
再來看看字符串轉(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] }
包裝類型
先來看一個(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í)是這樣的:
- 創(chuàng)建 String 類型的一個(gè)實(shí)例;
- 在實(shí)例上調(diào)用指定的方法;
- 銷毀這個(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
,來看下面的例子:
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ū)別,如果 hint
是 string
則優(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
是弱類型的語音,所以對(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)換
我們平時(shí)寫代碼的時(shí)候應(yīng)該盡量讓寫出來的代碼通俗易懂,讓別人能閱讀后知道你是要做什么,所以在對(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í),通常是由某些操作符或語句引起的,有下面這幾種情況:
- 隱式轉(zhuǎn)成布爾類型:
if(..)
語句中的條件判斷表達(dá)式。for(..;..;..)
語句中的條件判斷表達(dá)式(第二個(gè))。while(..)
和do..while(..)
循環(huán)中的條件判斷表達(dá)式。?:
中的條件判斷表達(dá)式。- 邏輯運(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ì)比較特殊:
- 當(dāng)一側(cè)為
String
類型,被識(shí)別為字符串拼接,并會(huì)優(yōu)先將另一側(cè)轉(zhuǎn)換為字符串類型。 - 當(dāng)一側(cè)為
Number
類型,另一側(cè)為原始類型,則將原始類型轉(zhuǎn)換為Number
類型。 - 當(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==true
或 x==false
這種,而應(yīng)該直接 if(x)
判斷。
3、 null
和 undefined
: null==undefined
比較結(jié)果是 true
,除此之外, null
、 undefined
和其他任何結(jié)果的比較值都為 false
??梢哉J(rèn)為在 ==
的情況下, null
和 undefined
可以相互的進(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
來判斷值是什么原始類型的,會(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 類型
undefined
和undeclared
有什么區(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
類型可以正確判斷,其他都無法正確判斷具體是什么引用類型。
用instanceof檢測(cè)引用類型
我們知道 typeof
只能對(duì)部分原始類型進(jìn)行檢測(cè),對(duì)引用類型毫無辦法。 JavaScript
提供了一個(gè)操作符 instanceof
,我們來看下他是否能檢測(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()
來檢測(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]'
文章來源于公眾號(hào):大海我來了 ,作者布蘭
以上就是W3Cschool編程獅
關(guān)于初中級(jí)前端必須要知道的JS數(shù)據(jù)類型的相關(guān)介紹了,希望對(duì)大家有所幫助。