值類型轉(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á)式)。