App下載

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

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

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

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

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

變量的存儲結(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)存中的存儲模型如圖所示:

棧內(nèi)存

復(fù)制

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

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

復(fù)制

  1. letstr1='hello': 復(fù)制前,定義了一個變量 str1,并且給其賦值 hello,這個時候 hello 這個字符串就會在棧內(nèi)存中被分配一塊空間進行存儲,然后變量 str1 會指向這個內(nèi)存地址;
  2. letstr2=str1:復(fù)制后,把 str1 的值賦值給 str2,這個時候會在棧中新開辟一塊空間用來存儲 str2 的值;
  3. str2='world':給 str2 賦值了一個新的字符串 world,那么將新建一塊內(nèi)存用來存儲 world,同時 str2 原來的值 hello 的內(nèi)存空間因為沒有變量所引用,所以一段時間后將被當(dāng)成垃圾回收;
  4. console.log(str1):因為 str1str2 的棧內(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ù)制的時候是把變量所指向的引用地址進行賦值給新的變量,所以復(fù)制后 person1person2 都指向堆內(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 傻傻分不清?

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 在轉(zhuǎn)成數(shù)字類型的時候會變成 0,而 undefined 會變成 NaN:

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

認識新的原始類型 Symbol

Symbol 值表示唯一標(biāo)識符,是 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') 的時候,就會找到全局的那個描述為 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 被設(shè)計出來其實最大的初衷就是用于對象的屬性鍵:

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

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

注意一點,如果用 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、 0o120xa;其中的 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, 則 nNaN :

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 標(biāo)準的,用 64 位來存儲一個數(shù)字, 64 位又被分隔成 1、 1152 位來分別表示符號位、指數(shù)位和尾數(shù)位。

為什么 0.1 + 0.2 不等于 0.3

比如十進制的 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 標(biāo)準規(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ù) n1n2 是否相等可以如下操作:

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

Number.EPSILONES6 中的誤差精度,實際值可以認為等于 $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í)行過程其實是這樣的:

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

用代碼體現(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ū)別,如果 hintstring 則優(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)換規(guī)則

顯示類型轉(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)成布爾類型:

  1. if(..)語句中的條件判斷表達式。
  2. for(..;..;..)語句中的條件判斷表達式(第二個)。
  3. while(..)do..while(..) 循環(huán)中的條件判斷表達式。
  4. ?:中的條件判斷表達式。
  5. 邏輯運算符 || (邏輯或)和 && (邏輯與)左邊的操作數(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ù)字類型,但是對 + 操作符會比較特殊:

  1. 當(dāng)一側(cè)為 String 類型,被識別為字符串拼接,并會優(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]'

  • 寬松相等和嚴格相等

寬松相等( ==)和嚴格相等( ===)在面試的時候經(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==truex==false 這種,而應(yīng)該直接 if(x) 判斷。

3、 nullundefined: null==undefined 比較結(jié)果是 true,除此之外, null、 undefined 和其他任何結(jié)果的比較值都為 false。可以認為在 == 的情況下, nullundefined 可以相互的進行隱式類型轉(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 類型

undefinedundeclared 有什么區(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)介紹了,希望對大家有所幫助。

0 人點贊