你不知道的JavaScript之強(qiáng)制類型轉(zhuǎn)換

值類型轉(zhuǎn)換

將值從一種類型轉(zhuǎn)換為另一種類型通常稱為類型轉(zhuǎn)換( type casting),這是顯式的情況;隱式的情況稱為強(qiáng)制類型轉(zhuǎn)換( coercion)。

JavaScript 中的強(qiáng)制類型轉(zhuǎn)換總是返回標(biāo)量基本類型值,如字符串、數(shù)字和布爾值,不會(huì)返回對(duì)象和函數(shù)。

var num = 1
String(num)  // '1'
typeof String(num)  // 'string'

隱式類型轉(zhuǎn)換與顯式類型轉(zhuǎn)換

// 將數(shù)值類型的值轉(zhuǎn)換成字符串
var a = 42
var b = a + ''  // 隱式類型轉(zhuǎn)換
var b = String(a)  // 顯式類型轉(zhuǎn)換

這里的“顯式”和“隱式”以及“明顯的副作用(sideEffect)”和“隱藏的副作用”,都是相對(duì)而言的。要是你明白 a + "" 是怎么回事,它對(duì)你來(lái)說(shuō)就是“顯式”的。相反,如果你不知道 String(..) 可以用來(lái)做字符串強(qiáng)制類型轉(zhuǎn)換,它對(duì)你來(lái)說(shuō)可能就是“隱式”的。

抽象值操作

toString

非字符串轉(zhuǎn)換為字符串

var a = 42
a.toString()  // '42'

var a = [1, 2, 3]
a.toString()  // '1,2,3'

除了 undefined 和 null,其他類型的值都有 toString 方法

let a = 123
a.toString()    // '123'

let b = [1, 2, 3]
b.toString()    // '1, 2, 3'

let c = {}
c.toString()    // '[object Object]'

let d = function () {}
d.toString()    // 'function () {}'

let e = true
e.toString()    // 'true'

Object.prototype.toString()

任何類型的值可以通過(guò) call 的方式調(diào)用,返回 '[object /該值的基本類型/]',表示任何類型的值都是由對(duì)象構(gòu)造的

let a = 123
Object.prototype.toString.call(a)    // '[object Number]'

let b = [1, 2, 3]
Object.prototype.toString.call(b)    // '[object Array]'

let c = {}
Object.prototype.toString.call(c)    // '[object Object]'

let d = function () {}
Object.prototype.toString.call(d)    // '[object Function]'

let e = true
Object.prototype.toString.call(e)    // '[object Boolean]'

Object.prototype.toString.call(null)    // '[object Null]'

Object.prototype.toString.call(undefined)   // '[object Undefined]'

不安全的JSON值

  • Function
const foo = function () {}
JSON.stringify(foo)   // undefined
  • RegExp
const reg = /w/
JSON.stringify(reg)   // "{}"

JSON 序列化

所有安全的 JSON 值( JSON-safe)都可以使用 JSON.stringify(..) 字符串化。安全的 JSON 值是指能夠呈現(xiàn)為有效 JSON 格式的值。

JSON.stringify(42)  // "42"
JSON.stringify("42")  // ""42""
JSON.stringify(null)  // "null"
JSON.stringify(true)  // "true"
JSON.stringify(undefined)  // "undefined"
JSON.stringify(function() {})  // "undefined"

JSON.stringify() 規(guī)則

  • undefined、 function、 symbol( ES6+)和包含循環(huán)引用(對(duì)象之間相互引用,形成一個(gè)無(wú)限循環(huán))的對(duì)象都不符合 JSON結(jié)構(gòu)標(biāo)準(zhǔn),支持 JSON 的語(yǔ)言無(wú)法處理它們。
  • JSON.stringify(..) 在對(duì)象中遇到 undefined、 function 和 symbol 時(shí)會(huì)自動(dòng)將其忽略,在數(shù)組中則會(huì)返回 null(以保證單元位置不變)。
  • 如果對(duì)象中定義了 toJSON() 方法, JSON 字符串化時(shí)會(huì)首先調(diào)用該方法,然后用它的返回值來(lái)進(jìn)行序列化。
  • toJSON() 方法存在于日期對(duì)象中

我們可以向 JSON.stringify(..) 傳遞一個(gè)可選參數(shù) replacer,它可以是數(shù)組或者函數(shù),用
來(lái)指定對(duì)象序列化過(guò)程中哪些屬性應(yīng)該被處理,哪些應(yīng)該被排除,和 toJSON() 很像。

如果 replacer 是一個(gè)數(shù)組,那么它必須是一個(gè)字符串?dāng)?shù)組,其中包含序列化要處理的對(duì)象的屬性名稱,除此之外其他的屬性則被忽略。

var obj = {
  a: 1,
  b: 2,
  c: 3,
}
JSON.stringify(obj, ['a', 'b'])  // "{"a":"1","b":"2"}"

JSON.string 還有一個(gè)可選參數(shù) space,用來(lái)指定輸出的縮進(jìn)格式。
JSON.stringify(value, ?replacer, ?space)

JSON.stringify(..) 并不是強(qiáng)制類型轉(zhuǎn)換。

與 toString() 的淵源

  • 字符串、數(shù)字、布爾值和 null 的 JSON.stringify(..) 規(guī)則與 ToString 基本相同。
  • 如果傳遞給 JSON.stringify(..) 的對(duì)象中定義了 toJSON() 方法,那么該方法會(huì)在字符串化前調(diào)用,以便將對(duì)象轉(zhuǎn)換為安全的 JSON 值。

toNumber

有時(shí)我們需要將非數(shù)字值當(dāng)作數(shù)字來(lái)使用,比如數(shù)學(xué)運(yùn)算。為此 ES5 規(guī)范在 9.3 節(jié)定義了抽象操作 ToNumber。

ToNumber 對(duì)字符串的處理基本遵循數(shù)字常量的相關(guān)規(guī)則 / 語(yǔ)法。
處理失敗時(shí)返回 NaN(處理數(shù)字常量失敗時(shí)會(huì)產(chǎn)生語(yǔ)法錯(cuò)誤)。

對(duì)象(包括數(shù)組)會(huì)首先被轉(zhuǎn)換為相應(yīng)的基本類型值,如果返回的是非數(shù)字的基本類型值,則再遵循以上規(guī)則將其強(qiáng)制轉(zhuǎn)換為數(shù)字。

var obj = {
  a: 1
}
obj.valueOf()  // {a:1}
Number(obj)  // NaN  處理失敗的情況
obj.toString()  // "[object Object]"

var arr = [1, 2]
arr.valueOf()  // [1, 2]
Number(arr)  // NaN
arr.toString()  // "1,2"

為了將值轉(zhuǎn)換為相應(yīng)的基本類型值,抽象操作 ToPrimitive(參見(jiàn) ES5 規(guī)范 9.1 節(jié))會(huì)首先(通過(guò)內(nèi)部操作 DefaultValue,參見(jiàn) ES5 規(guī)范 8.12.8 節(jié))檢查該值是否有 valueOf() 方法。
如果有并且返回基本類型值,就使用該值進(jìn)行強(qiáng)制類型轉(zhuǎn)換。如果沒(méi)有就使用 toString()的返回值(如果存在)來(lái)進(jìn)行強(qiáng)制類型轉(zhuǎn)換。
如果 valueOf() 和 toString() 均不返回基本類型值,會(huì)產(chǎn)生 TypeError 錯(cuò)誤。

[[PrimitiveValue]]

var num = new Number(42) || Object(42)  // 兩種寫法效果一樣
num
Number {42} => [[PrimitiveValue: 42]]
num.valueOf()  // 42
Number(num)  // 42
+ num  // 類似 Number
/**
 * 這里執(zhí)行了三步
 * 1、尋找該值的 valueOf() 方法,發(fā)現(xiàn)有
 * 2、調(diào)用該方法,返回 42
 * 3、對(duì)該值進(jìn)行強(qiáng)制類型轉(zhuǎn)換,轉(zhuǎn)換為 42
 */

從 ES5 開(kāi)始,使用 Object.create(null) 創(chuàng)建的對(duì)象 [[Prototype]] 屬性為 null,并且沒(méi)有 valueOf() 和 toString() 方法,因此無(wú)法進(jìn)行強(qiáng)制類型轉(zhuǎn)換。

var nullValue = Object.create(null)
// 沒(méi)有 valueOf() 和 toString() 方法
// 無(wú)法進(jìn)行強(qiáng)制類型轉(zhuǎn)換
Boolean(nullValue)  // Uncaught TypeError: Cannot convert object to primitive value
// 模擬 Number 的運(yùn)作過(guò)程
var a = {
  valueOf: function () {
    return 42
  }
}
var b = {
  toString: function () {
    return '42'
  }
}
var c = {
  toString: function () {
    return {}
  }
}

Number(a)  // 42
Number(b)  // 42
Number(c)  // Uncaught TypeError: Cannot convert object to primitive value

ToBoolean

首先也是最重要的一點(diǎn)是, JavaScript 中有兩個(gè)關(guān)鍵詞 true 和 false,分別代表布爾類型中的真和假。我們常誤以為數(shù)值 1 和 0 分別等同于 true 和 false。在有些語(yǔ)言中可能是這樣,但在 JavaScript 中布爾值和數(shù)字是不一樣的。雖然我們可以將 1 強(qiáng)制類型轉(zhuǎn)換為 true,將 0 強(qiáng)制類型轉(zhuǎn)換為 false,反之亦然,但它們并不是一回事。

假值(falsy value):可以強(qiáng)制轉(zhuǎn)換為 false 的值

假值:

  • false
  • ''
  • 0
  • undefined
  • null
  • NaN

eq: 假值以外的都是真值

特殊的假值:假值對(duì)象(falsy object)

  • Boolean({}) // true
  • Boolean([]) // true
  • new Boolean(false) // true
  • new Boolean(0) // true

真值(truthy value): 除假值外的所有值

var a = []
var b = {}
var c = function () {}
Boolean(a) && Boolean(b) && Boolean(c)  // true

顯示強(qiáng)制類型轉(zhuǎn)換

顯式強(qiáng)制類型轉(zhuǎn)換是那些顯而易見(jiàn)的類型轉(zhuǎn)換,很多類型轉(zhuǎn)換都屬于此列。我們?cè)诰幋a時(shí)應(yīng)盡可能地將類型轉(zhuǎn)換表達(dá)清楚,以免給別人留坑。類型轉(zhuǎn)換越清晰,代碼可讀性越高,更容易理解。

字符串與數(shù)字之間的類型轉(zhuǎn)換

字符串和數(shù)字之間的轉(zhuǎn)換是通過(guò) String(..) 和 Number(..) 這兩個(gè)內(nèi)建函數(shù)來(lái)實(shí)現(xiàn)的。
它們前面沒(méi)有 new 關(guān)鍵字,并不創(chuàng)建封裝對(duì)象。

var a = 42
String(a)  // "a"
var b = '3.14'
Number(b)  // 3.14

String(...) 將值轉(zhuǎn)換為字符串基本類型, Number(...) 將值轉(zhuǎn)換為數(shù)值基本類型。

var a = 42
a.toString()  // "42"

var b = '3.14'
+b  // 3.14

a.toString() 是顯式的,不過(guò)其中涉及隱式轉(zhuǎn)換。
因?yàn)椤oString() 對(duì) 42 這樣的基本類型值不適用,所以 JavaScript 引擎會(huì)自動(dòng)為 42 創(chuàng)建一個(gè)封裝對(duì)象(參見(jiàn)第 3 章),然后對(duì)該對(duì)象調(diào)用 toString()。這里顯式轉(zhuǎn)換中含有隱式轉(zhuǎn)換。
+c 是 + 運(yùn)算符的一元( unary)形式(即只有一個(gè)操作數(shù))。 + 運(yùn)算符顯式地將 c 轉(zhuǎn)換為數(shù)字,而非數(shù)字加法運(yùn)算

var a = '2'
var b = 1 + a  // '12' 字符串拼接
var c = 1 + +a  // 3 顯式強(qiáng)制類型轉(zhuǎn)換

經(jīng)典:+c 是顯式還是隱式,取決于你自己的理解和經(jīng)驗(yàn)。如果你已然知道一元運(yùn)算符 + 會(huì)將操作數(shù)顯式強(qiáng)制類型轉(zhuǎn)換為數(shù)字,那它就是顯式的。如果不明就里的話,它就是隱式強(qiáng)制類型轉(zhuǎn)換,讓你摸不著頭腦。

  • 操作符
var a = '42'
- -a  // 可以達(dá)到與 +a 同樣的效果

為何用 - -a 這種略顯怪異的寫法,是因?yàn)?--a 有遞減的意思

操作符的應(yīng)用

  • 日期顯示轉(zhuǎn)換為數(shù)字
var date = new Date()
var timeStamp = +date  // 這里使用的是,顯式強(qiáng)制類型轉(zhuǎn)換

var timeStamp = date.getTime()  // 更為顯式的寫法,調(diào)用方法,更容易讓人理解
var timeStamp = Date.now()  // es5 提供的更為合理的寫法

想起一句話:評(píng)判一個(gè)代碼的好壞,并不是看他語(yǔ)法用的多高級(jí),不是看他用了多少鮮為人知的代碼,而是那種能讓新人一看就能懂的代碼。

  • 奇特的 ~ 操作符(字位操作“非”)

它首先將值強(qiáng)制類型轉(zhuǎn)換為 32 位數(shù)字,然后執(zhí)行字位操作“非”(對(duì)每一個(gè)字位進(jìn)行反轉(zhuǎn))。

~42  // -43
-(42+1)  // -43
~(-42) => -(-42+1) => 41

在 -(x+1) 中唯一能夠得到 0(或者嚴(yán)格說(shuō)是 -0)的 x 值是 -1。也就是說(shuō)如果 x 為 -1 時(shí), ~和一些數(shù)字值在一起會(huì)返回假值 0,其他情況則返回真值。

~ 與 indexOf() 配合使用
indexOf() -- 返回一個(gè)字符串在另一個(gè)字符串中的位置,如果沒(méi)找到,則返回 -1
該方法在字符串中搜索指定的子字符串,如果找到就返回子字符串所在的位置(從 0 開(kāi)始),否則返回 -1。

var str = 'hello world'
if (str.indexOf('o') !== -1) {  // true
  ...
}

if (str.indexOf('w') >= 0) {  // true
  ...
}

1>= 0 和 == -1 這樣的寫法不是很好,稱為“抽象滲漏”,意思是在代碼中暴露了底層的實(shí)現(xiàn)細(xì)節(jié),這里是指用 -1 作為失敗時(shí)的返回值,這些細(xì)節(jié)應(yīng)該被屏蔽掉。

// 更簡(jiǎn)便的寫法,看起來(lái)簡(jiǎn)便,但是對(duì)于不懂的人來(lái)說(shuō)就是耐人尋味的代碼
var str = 'hello world'
if (~str.indexOf('lo')) {   // -4 true
  ...
}

if (~str.indexOf('lolo')) {  // 0 false
  ...
}

如果 indexOf(..) 返回 -1, ~ 將其轉(zhuǎn)換為假值 0,其他情況一律轉(zhuǎn)換為真值。
從技術(shù)角度來(lái)說(shuō), if (~a.indexOf(..)) 仍然是對(duì) indexOf(..) 的返回結(jié)果進(jìn)行隱式強(qiáng)制類型轉(zhuǎn)換, 0 轉(zhuǎn)換為 false,其他情況轉(zhuǎn)換為 true。但我覺(jué)得 ~ 更像顯式強(qiáng)制類型轉(zhuǎn)換,前提是我對(duì)它有充分的理解。

  • 字位截除
    Math.floor(x) => 找到小于 x,而且離 x 最近的整數(shù)

~~ 后面接正數(shù)的時(shí)候與 Math.floor() 相同, 負(fù)數(shù)時(shí)是找到離 x 最近的大于 x 的整數(shù)

Math.floor(4.5)  // 4
Math.floor(-4.5)  // -5

~~4.5  // 4
~~-4.5  // -4

顯式解析數(shù)字字符串

解析字符串中的數(shù)字和將字符串強(qiáng)制類型轉(zhuǎn)換為數(shù)字的返回結(jié)果都是數(shù)字。但解析和轉(zhuǎn)換兩者之間還是有明顯的差別。

析允許字符串中含有非數(shù)字字符,解析按從左到右的順序,如果遇到非數(shù)字字符就停止。而轉(zhuǎn)換不允許出現(xiàn)非數(shù)字字符,否則會(huì)失敗并返回 NaN。

解析和轉(zhuǎn)換之間不是相互替代的關(guān)系。它們雖然類似,但各有各的用途。如果字符串右邊的非數(shù)字字符不影響結(jié)果,就可以使用解析。而轉(zhuǎn)換要求字符串中所有的字符都是數(shù)字,像 "42px" 這樣的字符串就不行。

var a = '42'
var b = '42px'

parseInt(a)  // 解析字符串 42
Number(a)  // 顯式強(qiáng)制類型轉(zhuǎn)換 42

parseInt(b)  // 42
Number(b)  // 轉(zhuǎn)換失敗,返回 NaN

parseInt(s, ?radix) -- 第一個(gè)參數(shù)是要解析的字符串,第二個(gè)參數(shù)是轉(zhuǎn)變之后的數(shù)字基底(2進(jìn)制、10進(jìn)制),默認(rèn)十進(jìn)制
從 ES5 開(kāi)始 parseInt(..) 默認(rèn)轉(zhuǎn)換為十進(jìn)制數(shù),除非另外指定。如果你的代碼需要在 ES5 之前的環(huán)境運(yùn)行,請(qǐng)記得將第二個(gè)參數(shù)設(shè)置為 10。

注:parseInt(..) 先將參數(shù)強(qiáng)制類型轉(zhuǎn)換為字符串再進(jìn)行解析,這樣做沒(méi)有任何問(wèn)題。因?yàn)閭鬟f錯(cuò)誤的參數(shù)而得到錯(cuò)誤的結(jié)果,并不能歸咎于函數(shù)本身。

parseFloat() 解析浮點(diǎn)數(shù)

顯式轉(zhuǎn)換為布爾值

使用 Boolean(x)

Boolean(undefined)  // false
Boolean(null)  // false

Boolean({})  // true
Boolean([])  // true

一元運(yùn)算符 ! 顯式的將值轉(zhuǎn)換為其自身布爾類型的相反的值,根據(jù)這個(gè)特性,使用 !! 顯式的將值轉(zhuǎn)換為對(duì)應(yīng)的布爾值。

if(xxx) {} 背后的原理:

在 if(..).. 這樣的布爾值上下文中,如果沒(méi)有使用 Boolean(..) 和 !!,就會(huì)自動(dòng)隱式地進(jìn)行 ToBoolean 轉(zhuǎn)換。建議使用 Boolean(..) 和 !! 來(lái)進(jìn)行顯式轉(zhuǎn)換以便讓代碼更清晰易讀。

JSON.stringify()

var arr = [1, function () {}, 3]
JSON.stringify(arr)  // "[1,null,3]"

顯式 ToBoolean 的另外一個(gè)用處,是在 JSON 序列化過(guò)程中將值強(qiáng)制類型轉(zhuǎn)換為 true 或false,而不是只顯示 null

顯式轉(zhuǎn)換布爾值在三元運(yùn)算符中的應(yīng)用:

var a = 42
var b = a ? true : false  // true
// 更簡(jiǎn)便的寫法
var b = !!a
var b = Boolean(a)

這里涉及隱式強(qiáng)制類型轉(zhuǎn)換,因?yàn)?a 要首先被強(qiáng)制類型轉(zhuǎn)換為布爾值才能進(jìn)行條件判斷。這種情況稱為“顯式的隱式”,有百害而無(wú)一益,我們應(yīng)徹底杜絕。
所以在使用三元運(yùn)算符的時(shí)候盡量使用顯式強(qiáng)制類型轉(zhuǎn)換對(duì)待判斷值做顯式處理,更容易讓人理解(!!a Boolean(a))。

隱式強(qiáng)制類型轉(zhuǎn)換

隱式強(qiáng)制類型轉(zhuǎn)換指的是那些隱蔽的強(qiáng)制類型轉(zhuǎn)換,副作用也不是很明顯。換句話說(shuō),你自己覺(jué)得不夠明顯的強(qiáng)制類型轉(zhuǎn)換都可以算作隱式強(qiáng)制類型轉(zhuǎn)換。
顯式強(qiáng)制類型轉(zhuǎn)換旨在讓代碼更加清晰易讀,而隱式強(qiáng)制類型轉(zhuǎn)換看起來(lái)就像是它的對(duì)立面,會(huì)讓代碼變得晦澀難懂。

字符串與數(shù)字之間的隱式強(qiáng)制類型轉(zhuǎn)換

  • 操作符規(guī)則
    如果兩個(gè)操作數(shù)都是數(shù)字,將執(zhí)行加法操作;
    如果有一個(gè)操作數(shù)是字符串(或者說(shuō)能被轉(zhuǎn)換成字符串),將執(zhí)行字符串拼接操作;

根據(jù) ES5 規(guī)范 11.6.1 節(jié),如果某個(gè)操作數(shù)是字符串或者能夠通過(guò)以下步驟轉(zhuǎn)換為字符串的話, + 將進(jìn)行拼接操作。如果其中一個(gè)操作數(shù)是對(duì)象(包括數(shù)組),則首先對(duì)其調(diào)用 ToPrimitive 抽象操作(規(guī)范 9.1 節(jié)),該抽象操作再調(diào)用 [[DefaultValue]](規(guī)范 8.12.8節(jié)),以數(shù)字作為上下文。

var a = 42
var b = '42'

var c = [4, 2]
var d = [2]

a + 0  // 42
b + 0  // '420'

c + d  // "4,22"

對(duì)于兩個(gè)數(shù)組或者一個(gè)數(shù)組和一個(gè)對(duì)象相加,會(huì)執(zhí)行以下操作:

  • c.toString() // "4,2"
  • d.toString() // "2"
  • "4,2" + "2" // "4,22"
  • 數(shù)字減法運(yùn)算符
    為了執(zhí)行減法運(yùn)算,左右兩邊的數(shù)都要轉(zhuǎn)換成數(shù)字,它們首先被轉(zhuǎn)換為字符串(通過(guò)強(qiáng)制類型轉(zhuǎn)換toString()),然后再轉(zhuǎn)換為數(shù)字。

布爾值到數(shù)字的隱式強(qiáng)制類型轉(zhuǎn)換

隱式強(qiáng)制類型轉(zhuǎn)換為布爾值

發(fā)生布爾值隱式強(qiáng)制類型轉(zhuǎn)換的情況:

  • if (..) 語(yǔ)句中的條件判斷表達(dá)式。
  • for ( .. ; .. ; .. ) 語(yǔ)句中的條件判斷表達(dá)式(第二個(gè))。
  • while (..) 和 do..while(..) 循環(huán)中的條件判斷表達(dá)式。
  • ? : 中的條件判斷表達(dá)式。
  • 邏輯運(yùn)算符 ||(邏輯或)和 &&(邏輯與)左邊的操作數(shù)(作為條件判斷表達(dá)式)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容