JavaScript中有哪些數(shù)據(jù)類型?
計算機世界中定義的數(shù)據(jù)類型其實就是為了描述現(xiàn)實世界中存在的事實而定義的。比如我們用人來舉例:
- 有沒有人在房間里?這里的有和沒有就是是或者非的概念,在
JS
中對應(yīng)Boolean
類型,true
表示是,false
表示非; - 有幾個人在房間里?這里的幾個表示的是一個量級概念,在
JS
中對應(yīng)Number
類型,包含整數(shù)和浮點數(shù),還有一些特殊的值,比如:-Infinity
表示負無窮大、+Infinity
表示正無窮大、NaN
表示不是一個數(shù)字; - 房間里的這些人都是我的朋友。這是一句陳述語句,這種文本類的信息將會以字符串形式進行存儲,在
JS
中對應(yīng)String
類型; - 房間里沒有人。這里的沒有代表無和空的概念,在
JS
中null
和undefined
都可以表示這個意思; - 現(xiàn)實世界中所有人都是獨一無二的,這在
JS
中對應(yīng)Symbol
類型,表示唯一且不可改變; Number
所表示的整數(shù)是有范圍的,超出范圍的數(shù)據(jù)就沒法用Number
表示了,于是ES10
中提出了一種新的數(shù)據(jù)類型BigInt
,能表示任何位數(shù)的整數(shù);- 以上提到的
Boolean
、Number
、String
、null
、undefined
、Symbol
和BigInt
等7種類型都是JavaScript
中的原始類型,還有一種是非原始類型叫做對象類型;比如:一個人是對象,這個人有名字、性別、年齡等;
let person = {
name: 'bubuzou',
sex: 'male',
age: 26,}
為什么要區(qū)分原始類型和對象類型?他們之間有什么區(qū)別?
原始類型的不可變性
在回答這個問題之前,我們先看一下變量在內(nèi)存中是如何存儲的:
let name1 = 'bubuzou'
let name2 = name1.concat('.com')
console.log(name1) // 'bubuzou'
執(zhí)行完上面這段代碼,我們發(fā)現(xiàn)變量 name1
的值還是不變,依然是 bubuzou
。這就說明了字符串的不可變性。但是你看了下面的這段代碼,你就會產(chǎn)生疑問了:
let name1 = 'bubuzou'
name1 += '.com'
console.log(name1) // 'bubuzou.com'
你說字符串是不可變的,那現(xiàn)在不是變了嘛? 其實這只是變量的值變了,但是存在內(nèi)存中的字符串依然不變。這就涉及到變量在內(nèi)存中的存儲了。 在 JavaScript
中,變量在內(nèi)存中有2種存儲方式:存在棧中和存在堆中。那么棧內(nèi)存和堆內(nèi)存有啥區(qū)別呢?
棧內(nèi)存:
- 順序存儲結(jié)構(gòu),特點是先進后出。就像一個兵乒球盒子一樣,兵乒球從外面一個個的放入盒子里,最先取出來的一定是最后放入盒子的那個。
- 存儲空間固定
- 可以直接操作其保存的值,執(zhí)行效率高
堆內(nèi)存:
- 無序的存儲結(jié)構(gòu)
- 存儲空間可以動態(tài)變化
- 無法直接操作其內(nèi)部的存儲,需要通過引用地址操作
了解完變量在內(nèi)存中的存儲方式有2種,那我們繼續(xù)以上面那串代碼為例,畫出變量的存儲結(jié)構(gòu)圖:
然后我們可以描述下當(dāng)計算機執(zhí)行這段代碼時候的發(fā)生了什么?首先定義了一個變量 name1
并且給其賦值 bubuzou
這個時候就會在內(nèi)存中開辟一塊空間用來存儲字符串 bubuzou
,然后變量指向了這個內(nèi)存空間。然后再執(zhí)行第二行代碼 letname2=name1.concat('.com')
這里的拼接操作其實是產(chǎn)生了一個新字符串 bubuzou.com
,所以又會為這個新字符串創(chuàng)建一塊新內(nèi)存,并且把定義的變量 name2
指向這個內(nèi)存地址。 所以我們看到其實整個操作 bubuzou
這個字符串所在的內(nèi)存其實是沒有變化的,即使在第二段代碼中執(zhí)行了 name1+='.com'
操作,其實也只是變量 name1
指向了新的字符串 bubuzou.com
而已,舊的字符串 bubuzou
依然存在內(nèi)存中,不過一段時間后由于該字符串沒有被變量所引用,所以會被當(dāng)成垃圾進行回收,從而釋放掉該塊內(nèi)存空間。
從而我們得出結(jié)論:原始類型的值都是固定的,而對象類型則是由原始類型的鍵值對組合成一個復(fù)雜的對象;他們在內(nèi)存中的存儲方式是不一樣的,原始類型的值直接存在棧內(nèi)存中,而對象類型的實際值是存在堆內(nèi)存中的,在棧內(nèi)存中保存了一份引用地址,這個地址指向堆內(nèi)存中的實際值,所以對象類型又習(xí)慣被叫做引用類型。
想一個問題為什么引用類型的值要存儲到堆內(nèi)存中?能不能存到棧內(nèi)存中呢?答案一:因為引用類型大小不固定,而棧的大小是固定的,堆空間的大小是可以動態(tài)變化的,所以引用類型的值適合存在堆中;答案二:在代碼執(zhí)行過程中需要頻繁的切換執(zhí)行上下文的時候,如果把引用類型的值存到棧中,將會造成非常大的內(nèi)存開銷。
比較
當(dāng)我們對兩個變量進行比較的時候,不同類型的變量是有不同表現(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個字符串變量和2個對象變量,他們都長一模一樣,但是字符串變量會相等,對象變量卻不相等。這是因為在 JavaScript
中,原型類型進行比較的時候比較的是存在棧中的值是否相等;而引用類型進行比較的時候,是比較棧內(nèi)存中的引用地址是否相等。
如上幾個變量在內(nèi)存中的存儲模型如圖所示:
復(fù)制
變量進行復(fù)制的時候,原始類型和引用類型變量也是有區(qū)別的,來看下面的代碼:
let str1 = 'hello'
let str2 = str1
str2 = 'world'
console.log( str1 ) // 'hello'
letstr1='hello'
: 復(fù)制前,定義了一個變量str1
,并且給其賦值hello
,這個時候hello
這個字符串就會在棧內(nèi)存中被分配一塊空間進行存儲,然后變量str1
會指向這個內(nèi)存地址;letstr2=str1
:復(fù)制后,把str1
的值賦值給str2
,這個時候會在棧中新開辟一塊空間用來存儲str2
的值;str2='world'
:給str2
賦值了一個新的字符串world
,那么將新建一塊內(nèi)存用來存儲world
,同時str2
原來的值hello
的內(nèi)存空間因為沒有變量所引用,所以一段時間后將被當(dāng)成垃圾回收;console.log(str1)
:因為str1
和str2
的棧內(nèi)存地址是不一樣的,所以即使str2
的值被改變,也不會影響到str1
。
然后我們繼續(xù)往下,看下引用類型的復(fù)制:
let person1 = {
name: 'bubuzou',
age: 20
}
let person2 = person1
person2.name = 'bubuzou.com'
console.log( person1.name) // 'bubuzou.com'
原始類型進行復(fù)制的時候是變量的值進行重新賦值,而如上圖所示:引用類型進行復(fù)制的時候是把變量所指向的引用地址進行賦值給新的變量,所以復(fù)制后 person1
和 person2
都指向堆內(nèi)存中的同一個值,所以當(dāng)改變 person2.name
的時候, person1.name
也會被改變就是這個原因。
值傳遞和引用傳遞
先說一下結(jié)論,在 JavaScript
中,所有函數(shù)的參數(shù)傳遞都是按值進行傳遞的??慈缦麓a:
let name = 'bubuzou'
function changeName(name) {
name = 'bubuzou.com'
}
changeName(name)
console.log( name ) // 'bubuzou'
定義了一個變量 name
,并賦值為 bubuzou
,函數(shù)調(diào)用的時候傳入 name
,這個時候會在函數(shù)內(nèi)部創(chuàng)建一個局部變量 name
并且把全局變量的值 bubuzou
傳遞給他,這個操作其實是在內(nèi)存里新建了一塊空間用來存放局部變量的值,然后又把局部變量的值改成了 bubuzou.com
,這個時候其實內(nèi)存中會有3塊地址空間分別用來存放全局變量的值 bubuzou
、局部變量原來的值 bubuzou
、和局部變量新的值 bubuzou.com
;一旦函數(shù)調(diào)用結(jié)束,局部變量將被銷毀,一段時間后由于局部變量新舊值沒有變量引用,那這兩塊空間將被回收釋放;所以這個時候全局 name
的值依然是 bubuzou
。
再來看看引用類型的傳參,會不會有所不同呢?
let person = {
name: 'bubuzou'
}
function changePerosn(person) {
person.name = 'bubuzou.com'
}
changePerosn( person )
console.log( person.name ) // 'bubuzou.com'
引用類型進行函數(shù)傳參的時候,會把引用地址復(fù)制給局部變量,所以全局的 person
和函數(shù)內(nèi)部的局部變量 person
是指向同一個堆地址的,所以一旦一方改變,另一方也將被改變,所以至此我們是不是可以下結(jié)論說:當(dāng)函數(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
是引用傳遞的話,那就會自動指向值被改為 hello world
的新對象;事實上全局變量 person
的引用地址自始至終都沒有改變,倒是局部變量 person
的引用地址發(fā)生了改變。
null 和 undefined 傻傻分不清?
null
在 JavaScript
中自成一種原始類型,只有一個值 null
,表示無、空、值未知等特殊值。可以直接給一個變量賦值為 null
:
let s = null
undefined
和 null
一樣也是自成一種原始類型,表示定義了一個變量,但是沒有賦值,則這個變量的值就是 undefined
:
let s
console.log( s) // undefined
雖然可以給變量直接賦值為 undefined
也不會報錯,但是原則上如果一個變量值未定,或者表示空,則直接賦值為 null
比較合適,不建議給變量賦值 undefined
。 null
和 undefined
在進行邏輯判斷的時候都是會返回 false
的:
let a = null, b
console.log( a ? 'a' : b ? 'b' : 'c') // 'c'
null
在轉(zhuǎn)成數(shù)字類型的時候會變成 0
,而 undefined
會變成 NaN
:
let a = null, b
console.log( +null ) // 0
console.log( + b ) // NaN
認識新的原始類型 Symbol
Symbol
值表示唯一標識符,是 ES6
中新引進的一種原始類型。可以通過 Symbol()
來創(chuàng)建一個重要的值,也可以傳入描述值;其唯一性體現(xiàn)在即使是傳入一樣的描述,他們兩者之間也是不會相等的:
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()
可以在根據(jù)傳入的描述在全局范圍內(nèi)進行查找,如果沒找到則新建一個 Symbol
,并且返回;所以當(dāng)執(zhí)行第二行代碼 Symbol.for('bubuzou')
的時候,就會找到全局的那個描述為 bubuzou
的 Symbol
,所以這里 a
和 b
是會絕對相等的。
居然可以通過描述找到 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
被設(shè)計出來其實最大的初衷就是用于對象的屬性鍵:
let age = Symbol('20')
let person = {
name: 'bubuzou',
[age]: '20', // 在對象字面量中使用 `Symbol` 的時候需要使用中括號包起來
}
這里給 person
定義了一個 Symbol
作為屬性鍵的屬性,這個相比于用字符串作為屬性鍵有啥好處呢?最明顯的好處就是如果這個 person
對象是多個開發(fā)者進行開發(fā)維護,那么很容易再給 person
添加屬性的時候出現(xiàn)同名的,如果是用字符串作為屬性鍵那肯定是沖突了,但是如果用 Symbol
作為屬性鍵,就不會存在這個問題了,因為它是唯一標識符,所以可以使對象的屬性受到保護,不會被意外的訪問或者重寫。
注意一點,如果用 Symbol
作為對象的屬性鍵的時候, 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
存儲,也被稱為雙精度浮點數(shù),就是我們平常使用的數(shù)字,其范圍是 $2^{52}$ 到 -$2^{52}$;第二種類型是 BigInt
,能夠表示任意長度的整數(shù),包括超出 $2^{52}$ 到 -$2^{52}$ 這個范圍外的數(shù)。這里我們只介紹 Number
數(shù)字。
常規(guī)數(shù)字和特殊數(shù)字
對于一個常規(guī)的數(shù)字,我們直接寫即可,比如:
let age = 20
但是還有一種位數(shù)特別多的數(shù)字我們習(xí)慣用科學(xué)計數(shù)法的表示方法來寫:
let billion = 1000000000;
let b = 1e9
以上兩種寫法是一個意思, 1e9
表示 1 x $10^9$;如果是 1e-3
表示 1 / $10^3$ = 0.001。 在 JavaScript
中也可以用數(shù)字表示不同的進制,比如:十進制中的 10
在 二、八和十六進制中可以分別表示成 0b1010
、 0o12
和 0xa
;其中的 0b
是二進制前綴, 0o
是八進制前綴,而 ox
是十六進制的前綴。
我們也可以通過 toString(base)
方法來進行進制之間的轉(zhuǎn)換, base
是進制的基數(shù),表示幾進制,默認是 10
進制的,會返回一個轉(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個.
號不是寫錯了,而是必須是2個,否則會報SyntaxError
錯誤。第一個點表示小數(shù)點,第二個才是調(diào)用方法。點符號首先會被認為是數(shù)字常量的一部分,其次再被認為是屬性訪問符,如果只寫一個點的話,計算機無法知道這個是表示一個小數(shù)呢還是去調(diào)用函數(shù)。數(shù)字直接調(diào)用函數(shù)還可以有以下幾種寫法:
(10).toString(2) // 將10用括號包起來
10.0.toString(2) // 將10寫成10.0的形式
10 .toString(2) // 空格加上點符號調(diào)用
Number
類型除了常規(guī)數(shù)字之外,還包含了一些特殊的數(shù)字:
NaN
:表示不是一個數(shù)字,通常是由不合理的計算導(dǎo)致的結(jié)果,比如數(shù)字除以字符串1/'a'
;NaN
和任何數(shù)進行比較都是返回false
,包括他自己:NaN==NaN
會返回false
; 如何判斷一個數(shù)是不是NaN
呢?有四種方法:
方法一:通過 isNaN()
函數(shù),這個方法會對傳入的字符串也返回 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
, 則 n
是 NaN
:
let s = 1/'a'
console.log( s !== s ) // true
+Infinity
:表示正無窮大,比如1/0
計算的結(jié)果,-Infinity
表示負無窮大,比如-1/0
的結(jié)果。+0
和-0
,JavaScript
中的數(shù)字都有正負之分,包括零也是這樣,他們會絕對相等:
console.log( +0 === -0 ) // true
為什么 0.1 + 0.2 不等于 0.3
console.log( 0.1 + 0.2 == 0.3 ) // false
有沒有想過為什么上面的會不相等?因為數(shù)字在 JavaScript
內(nèi)部是用二進制進行存儲的,其遵循 IEEE754
標準的,用 64
位來存儲一個數(shù)字, 64
位又被分隔成 1
、 11
和 52
位來分別表示符號位、指數(shù)位和尾數(shù)位。
比如十進制的 0.1
轉(zhuǎn)成二進制后是多少?我們手動計算一下,十進制小數(shù)轉(zhuǎn)二進制小數(shù)的規(guī)則是“乘2取整,順序排列”,具體做法是:用2乘十進制小數(shù),可以得到積,將積的整數(shù)部分取出,再用2乘余下的小數(shù) 部分,又得到一個積,再將積的整數(shù)部分取出,如此進行,直到積中的小數(shù)部分為零,或者達到所要求的精度為止。
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...
我們這樣依次計算下去之后發(fā)現(xiàn)得到整數(shù)的順序排列是 0001100110011001100....
無限循環(huán),所以理論上十進制的 0.1
轉(zhuǎn)成二進制后會是一個無限小數(shù) 0.0001100110011001100...
,用科學(xué)計數(shù)法表示后將是 1.100110011001100...
x $2^{-4}$ ,但是由于 IEEE754
標準規(guī)定了一個數(shù)字的存儲位數(shù)只能是 64
位,有效位數(shù)是 52
位,所以將會對 1100110011001100....
這個無限數(shù)字進行舍入總共 52
位作為有效位,然后二進制的末尾取舍規(guī)則是看后一位數(shù)如果是 1
則進位,如果是 0
則直接舍去。那么由于 1100110011001100....
這串?dāng)?shù)字的第 53
位剛好是 1
,所以最終的會得到的數(shù)字是 1100110011001100110011001100110011001100110011001101
,即 1.100110011001100110011001100110011001100110011001101
x $2^{-4}$。 十進制轉(zhuǎn)二進制也可以用 toString
來進行轉(zhuǎn)化:
console.log( 0.1.toString(2) ) // '0.0001100110011001100110011001100110011001100110011001101'
我們發(fā)現(xiàn)十進制的 0.1
在轉(zhuǎn)化成二進制小數(shù)的時候發(fā)生了精度的丟失,由于進位,它比真實的值更大了。而 0.2
其實也有這樣的問題,也會發(fā)生精度的丟失,所以實際上 0.1+0.2
不會等于 0.3
:
console.log( 0.1 + 0.2 ) // 0.30000000000000004
那是不是沒辦法判斷兩個小數(shù)是否相等了呢?答案肯定是否定的,想要判斷2個小數(shù) n1
和 n2
是否相等可以如下操作:
- 方法一:兩小數(shù)之差的絕對值如果比
Number.EPSILON
還小,那么說明兩數(shù)是相等的。
Number.EPSILON
是 ES6
中的誤差精度,實際值可以認為等于 $2^{-52}$。
if ( Math.abs( n1 - n2 ) < Number.EPSILON ) {
console.log( 'n1 和 n2 相等' )
}
- 方法二:通過
toFixed(n)
對結(jié)果進行舍入,toFixed()
將會返回字符串,我們可以用 一元加+
將其轉(zhuǎn)成數(shù)字:
let sum = 0.1 + 0.2
console.log( +sum.toFixed(2) === 0.3 ) // true
數(shù)值的轉(zhuǎn)化
對數(shù)字進行操作的時候?qū)⒊3S龅綌?shù)值的舍入和字符串轉(zhuǎn)數(shù)字的問題,這里我們鞏固下基礎(chǔ)。先來看舍入的:
Math.floor()
,向下舍入,得到一個整數(shù):
Math.floor(2.2) // 2
Math.floor(2.8) // 2
Math.ceil()
,向上舍入,得到一個整數(shù):
Math.ceil(2.2) // 3
Math.ceil(2.8) // 3
Math.round()
,對第一位小數(shù)進行四舍五入:
Math.round(2.26) // 2
Math.round(2.46) // 2
Math.round(2.50) // 3
Number.prototype.toFixed(n)
,和Math.round()
一樣會進行四舍五入,將數(shù)字舍入到小數(shù)點后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
其實是一個無限小數(shù):
6.35.toFixed(20) // "6.34999999999999964473"
所以在 6.35.toFixed(1)
求值的時候會得到 6.3
。
再來看看字符串轉(zhuǎn)數(shù)字的情況:
Number(n)
或+n
,直接將n
進行嚴格轉(zhuǎn)化:
Number(' ') // 0
console.log( +'') // 0
Number('010') // 10
console.log( +'010' ) // 10
Number('12a') // NaN
console.log( +'12a' ) // NaN
parseInt()
,非嚴格轉(zhuǎn)化,從左到右解析字符串,遇到非數(shù)字就停止解析,并且把解析的數(shù)字返回:
parseInt('12a') // 12
parseInt('a12') // NaN
parseInt('') // NaN
parseInt('0xA') // 10,0x開頭的將會被當(dāng)成十六進制數(shù)
parseInt()
默認是用十進制去解析字符串的,其實他是支持傳入第二個參數(shù)的,表示要以多少進制的 基數(shù)去解析第一個參數(shù):
parseInt('1010', 2) // 10
parseInt('ff', 16) // 255
如何判斷一個數(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
}
引用類型
除了原始類型外,還有一個特別重要的類型:引用類型。高程里這樣描述他:引用類型是一種數(shù)據(jù)結(jié)構(gòu), 用于將數(shù)據(jù)和功能組織在一起。到目前為止,我們看到最多的引用類型就是 Object
,創(chuàng)建一個 Object
有兩種方式:
- 方式一:通過
new
操作符:
let person = new Object()
person.name = 'bubuzou'
person.age = 20
- 方式二:通過對象字面量,這是我們最喜歡用的方式:
let person = {
name: 'bubuzou',
age: 20
}
內(nèi)置的引用類型
除了 Object
外,在 JavaScript
中還有別的內(nèi)置的引用類型,比如:
Array
數(shù)組Date
日期RegExp
正則表達式Function
函數(shù)
他們的原型鏈的頂端都會指向 Object
:
let d = new Date()
console.log( d.__proto__.__proto__.constructor ) // ? Object() { [native code] }
包裝類型
先來看一個問題,為什么原始類型的變量沒有屬性和方法,但是卻能夠調(diào)用方法呢?
let str = 'bubuzou'
str.substring(0, 3) // 'bub'
因為 JavaScript
為了更好地操作原始類型,設(shè)計出了幾個對應(yīng)的包裝類型,他們分別是:
Boolean
Number
String
上面那串代碼的執(zhí)行過程其實是這樣的:
- 創(chuàng)建 String 類型的一個實例;
- 在實例上調(diào)用指定的方法;
- 銷毀這個實例
用代碼體現(xiàn)一下:
let str = new
String('bubuzou')
str.substring(0, 3)
str = null
原始類型調(diào)用函數(shù)其實就是自動進行了裝箱操作,將原始類型轉(zhuǎn)成了包裝類型,然后其實原始類型和包裝類型是有本質(zhì)區(qū)別的,原始類型是原始值,而包裝類型是對象實例:
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'
在拆箱操作的時候,默認會嘗試調(diào)用包裝類型的 toString()
和 valueOf()
方法,對于不同的 hint
調(diào)用順序會有所區(qū)別,如果 hint
是 string
則優(yōu)先調(diào)用 toString()
,否則的話,則優(yōu)先調(diào)用 valueOf()
。 默認情況下,一個 Object
對象具有 toString()
和 valueOf()
方法:
let obj = {}
console.log( obj.toString() ) // '[object Object]'
console.log( obj.valueOf() ) // {},valueOf會返回對象本身
類型裝換
Javascript
是弱類型的語音,所以對變量進行操作的時候經(jīng)常會發(fā)生類型的轉(zhuǎn)換,尤其是隱式類型轉(zhuǎn)換,可能會讓代碼執(zhí)行結(jié)果出乎意料之外,比如如下的代碼你能理解其執(zhí)行結(jié)果嘛?
[] + {} // '[object Object]'
{} + [] // 0
類型轉(zhuǎn)換規(guī)則
所以我們需要知道類型轉(zhuǎn)換的規(guī)則,以下整理出一個表格,列出了常見值和類型以及轉(zhuǎn)換之后的結(jié)果,僅供參考。
顯示類型轉(zhuǎn)換
我們平時寫代碼的時候應(yīng)該盡量讓寫出來的代碼通俗易懂,讓別人能閱讀后知道你是要做什么,所以在對類型進行判斷的時候應(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)換的冷門操作,有時候也挺管用的: 直接用一元加操作符獲取當(dāng)前時間的毫秒數(shù):
+new Date() // 1595517982686
用 ~
配合 indexOf()
將操作結(jié)果直接轉(zhuǎn)成布爾類型:
let str = 'bubuzou.com'
if (~str.indexOf('.com')) {
console.log( 'str如果包含了.com字符串,則會打印這句話' )
}
使用 ~~
對字符或數(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
的運行時,通常是由某些操作符或語句引起的,有下面這幾種情況:
- 隱式轉(zhuǎn)成布爾類型:
if(..)
語句中的條件判斷表達式。for(..;..;..)
語句中的條件判斷表達式(第二個)。while(..)
和do..while(..)
循環(huán)中的條件判斷表達式。?:
中的條件判斷表達式。- 邏輯運算符
||
(邏輯或)和&&
(邏輯與)左邊的操作數(shù)(作為條件判斷表達式)
if (42) {
console.log(42)
}
while ('bubuzou') {
console.log('bubuzou')
}
const c = null ? '存在' : '不存在' // '不存在'
上例中的非布爾值會被隱式強制類型轉(zhuǎn)換為布爾值以便執(zhí)行條件判斷。 需要特別注意的是 ||
和 &&
操作符。 ||
的操作過程是只有當(dāng)左邊的值返回 false
的時候才會對右邊進行求值且將它作為最后結(jié)果返回,類似 a?a:b
這種效果:
const a = 'a' || 'b' // 'a'
const b = '' || 'c' // 'c'
而 &&
的操作過程是只有當(dāng)左邊的值返回 true
的時候才對右邊進行求值且將右邊的值作為結(jié)果返回,類似 a?b:a
這種效果:
const a = 'a' && 'b' // 'b'
const b = '' && 'c' // ''
- 數(shù)學(xué)操作符
-*/
會對非數(shù)字類型的會優(yōu)先轉(zhuǎn)成數(shù)字類型,但是對+
操作符會比較特殊:
- 當(dāng)一側(cè)為
String
類型,被識別為字符串拼接,并會優(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]'
- 寬松相等和嚴格相等
寬松相等( ==
)和嚴格相等( ===
)在面試的時候經(jīng)常會被問到,而回答一般是 ==
是判斷值是否相等,而 ===
除了判斷值會不會相等之外還會判斷類型是否相等,這個答案不完全正確,更好的回答是: ==
在比較過程中允許發(fā)生隱式類型轉(zhuǎn)換,而 ===
不會。 那 ==
是怎么進行類型轉(zhuǎn)換的呢?
1、 數(shù)字和字符串比,字符串將轉(zhuǎn)成數(shù)字進行比較:
20 == '20' // true
20 === '20' // false
2、 別的類型和布爾類型比較,布爾類型將首先轉(zhuǎn)成數(shù)字進行比較, true
轉(zhuǎn)成數(shù)字 1
, false
轉(zhuǎn)成數(shù)字 0
,注意這個是非常容易出錯的一個點:
'bubuzou' == true // false
'0' == false // true
null == false // false,
undefined == false // false
[] == true // false
['1'] == true // true
所以寫代碼進行判斷的時候一定不要寫成 x==true
或 x==false
這種,而應(yīng)該直接 if(x)
判斷。
3、 null
和 undefined
: null==undefined
比較結(jié)果是 true
,除此之外, null
、 undefined
和其他任何結(jié)果的比較值都為 false
??梢哉J為在 ==
的情況下, null
和 undefined
可以相互的進行隱式類型轉(zhuǎn)換。
null == undefined // true
null == '' // false
null == 0 // false
null == false // false
undefined == '' // false
undefined == 0 // false
undefined == false // false
4、 原始類型和引用類型比較,引用類型會首先進行 ToPromitive
轉(zhuǎ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)先級比==高,所以![]先轉(zhuǎn)成布爾值變成false;即變成[] == false,false再轉(zhuǎn)成數(shù)字0,[]轉(zhuǎn)成數(shù)字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 類型
undefined
和undeclared
有什么區(qū)別?前者是表示在作用域中定義了但是沒有賦值的變量,而后者是表示在作用域中沒有定義的變量;分別表示undefined
未定義、undeclared
未聲明。
typeof
能夠?qū)υ碱愋瓦M行判斷,那是否也能判斷引用類型呢?
typeof [] // 'object'
typeof {} // 'object'
typeof new Date() // 'object'
typeof new RegExp() // 'object'
typeof new Function() // 'function'
從上面的結(jié)果我們可以得到這樣一個結(jié)論: typeof
對引用類型判斷的時候只有 function
類型可以正確判斷,其他都無法正確判斷具體是什么引用類型。
用instanceof檢測引用類型
我們知道 typeof
只能對部分原始類型進行檢測,對引用類型毫無辦法。 JavaScript
提供了一個操作符 instanceof
,我們來看下他是否能檢測引用類型:
[] instanceof Array // true
[] instanceof Object // true
我們發(fā)現(xiàn)數(shù)組即是 Array
的實例,也是 Object
的實例,因為所以引用類型原型鏈的終點都是 Object
,所以 Array
自然是 Object
的實例。那么我們得出結(jié)論: 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編程獅
關(guān)于初中級前端必須要知道的JS數(shù)據(jù)類型的相關(guān)介紹了,希望對大家有所幫助。