# YDKJS-强制类型转换

将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐式的情况称为强制类型转换(coercion)。

在 JavaScript 中的强制类型转换总是返回标量基本类型值,如字符串、数字和布尔值,不会返回对象和函数。

var a = 42
var b = a + '' // 隐式强制类型转换
var c = String(a) // 显式强制类型转换

也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)。

# ToString

抽象操作 ToString,它负责处理非字符串到字符串的强制类型转换。

基本类型值的字符串化规则为:null 转换为 "null"undefined 转换为 "undefined"true 转换为 "true"。数字的字符串化则遵循通用规则,不过那些极小和极大的数字会使用指数形式。

对普通对象来说,除非自行定义,否则 toString()(Object.prototype.toString())返回内部属性 [[Class]] 的值。

数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 "," 连接起来:

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

工具函数 JSON.stringify(..) 在将 JSON 对象序列化为字符串时也用到了 ToString。如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回值来进行序列化。

当其在对象中遇到 undefinedfunctionsymbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。对包含循环引用的对象执行 JSON.stringify(..) 会出错。

我们可以向 JSON.stringify(..) 传递一个可选参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和 toJSON() 很像。

# ToNumber

有时我们需要将非数字值当作数字来使用,其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。

ToNumber 对字符串的处理基本遵循数字常量的相关规则/语法。处理失败时返回 NaN(处理数字常量失败时会产生语法错误)。不同之处是 ToNumber 对以 0 开头的十六进制数并不按十六进制处理,而是按十进制。

对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,会首先检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。

var a = {
  valueOf: function() {
    return '42'
  },
}
var b = {
  toString: function() {
    return '42'
  },
}
var c = [4, 2]
c.toString = function() {
  return this.join('') // "42"
}

Number(a) // 42
Number(b) // 42
Number(c) // 42
Number('') // 0
Number([]) // 0
Number(['abc']) // NaN

如果 valueOf()toString() 均不返回基本类型值,会产生 TypeError 错误。

# ToBoolean

ES5 规范 9.2 节中定义了抽象操作 ToBoolean,列举了布尔强制类型转换所有可能出现的结果。以下这些是假值:

  • undefined
  • null
  • false
  • +0、-0 和 NaN
  • ""

真值就是假值列表之外的值。

浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来(exotic)值,这些被称为“假值对象”。

假值对象(比如 document.all 方法得到的结果)看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为 false

# 字符串和数字之间的显式转换

字符串和数字之间的转换是通过 String(..)Number(..) 这两个内建函数来实现的,请注意它们前面没有 new 关键字,并不创建封装对象。

String(..) 遵循前面讲过的 ToString 规则,将值转换为字符串基本类型。Number(..) 遵循前面讲过的 ToNumber 规则,将值转换为数字基本类型。

除了 String(..)Number(..) 以外,还有其他方法可以实现字符串和数字之间的显式转换:

var a = 42
var b = a.toString()
var c = '3.14'
var d = +c
b // "42"
d // 3.14

一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为 Unix 时间戳,以微秒为单位(从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间):

var timestamp = +new Date()
// JavaScript 有一处奇特的语法,即构造函数没有参数时可以不用带 ()
// var timestamp = +new Date

# 奇特的 ~ 运算符

字位运算符只适用于 32 位整数,运算符会强制操作数使用 32 位格式(先执行 ToNumber 强制类型转换,然后再执行 ToInt32)。

虽然严格说来并非强制类型转换(因为返回值类型并没有发生变化),但字位运算符(如 | 和 ~)和某些特殊数字一起使用时会产生类似强制类型转换的效果,返回另外一个数字。

例如 | 运算符(字位操作“或”)的空操作(no-op)0 | x,它仅执行 ToInt32 转换:

0 | -0 // 0
0 | NaN // 0
0 | Infinity // 0
0 | -Infinity // 0

以上这些特殊数字无法以 32 位格式呈现(因为它们来自 64 位 IEEE 754 标准),因此 ToInt32 返回 0。

源自早期的计算机科学和离散数学:~ 返回 2 的补码。这样 ~x 大致等同于 -(x+1)。很奇怪,但相对更容易说明问题:

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

-(x+1) 中唯一能够得到 0(或者严格说是 -0)的 x 值是 -1。也就是说如果 x 为 -1 时,~ 和一些数字值在一起会返回假值 0,其他情况则返回真值。

在使用 indexOf(...) 方法判断是否包含某个元素时,>= 0== -1 这样的写法不是很好,称为“抽象渗漏”,意思是在代码中暴露了底层的实现细节,这里是指用 -1 作为失败时的返回值,这些细节应该被屏蔽掉。

结合 运算符可以这样:

var str = 'Hello'

if (!~str.indexOf('variable')) {
  // true
  // 没有找到匹配
}

如果 indexOf(..) 返回 -1,~ 将其转换为假值 0,其他情况一律转换为真值。

另外,一些开发人员使用 ~~ 来截除数字值的小数部分,其中的第一个 ~ 执行 ToInt32 并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有字位反转回原值,最后得到的仍然是 ToInt32 的结果。

~~ 我们要多加注意。首先它只适用于 32 位数字,更重要的是它对负数的处理与 Math.floor(..) 不同。

Math.floor(-49.6) // -50
~~-49.6 // -49

~~x 能将值截除为一个 32 位整数,x | 0 也可以,而且看起来还更简洁。

# 显式解析数字字符串

解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字,但解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回 NaN。

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

Number(a) // 42
parseInt(a) // 42
Number(b) // NaN
parseInt(b) // 42

parseInt(..) 针对的是字符串值,非字符串参数会首先被强制类型转换为字符串,依赖这样的隐式强制类型转换并非上策,应该避免向 parseInt(..) 传递非字符串参数。

另外,在 ES5 之前如果没有第二个参数来指定转换的基数(又称为 radix),parseInt(..) 会根据字符串的第一个字符来自行决定基数。从 ES5 开始 parseInt(..) 默认转换为十进制数,

# 显式转换为布尔值

与前面的 String(..)Number(..) 一样,Boolean(..)(不带 new)是显式的 ToBoolean 强制类型转换:

var flag = Boolean() // 不传值将返回假值

一元运算符 ! 显式地将值强制类型转换为布尔值。但是它同时还将真值反转为假值(或者将假值反转为真值)。所以显式强制类型转换为布尔值最常用的方法是 !!

在三元运算符和 if(..).. 这样的布尔值上下文中,如果没有使用 Boolean(..)!!,就会自动隐式地进行 ToBoolean 转换。

建议使用 Boolean(..)!! 来进行显式转换以便让代码更清晰易读。

# 隐式强制类型转换

隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。它的的作用是减少冗余,让代码更简洁,但同时会让代码变得晦涩难懂。

有必要说一下的是:ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。

var s1 = Symbol('cool')
var s2 = Symbol('not cool')

String(s1) // "Symbol(cool)"
s2 + '' // TypeError

符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true)。

# 字符串和数字之间的隐式强制类型转换

通过重载,+ 运算符即能用于数字加法,也能用于字符串拼接。JavaScript 怎样来判断我们要执行的是哪个操作?

简单来说就是,如果 + 的其中一个操作数是字符串(或者可以转换得到字符串),则执行字符串拼接;否则执行数字加法。

对隐式强制类型转换来说,这意味着什么?我们可以将数字和空字符串 ""+ 来将其转换为字符串:

var a = 42
var b = a + ''
b // "42"

需要注意的是 a + "" 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()

对于 -/* 三个运算符,由于只能用于数字运算,所以会将非数字强制类型转换为数字。

# 布尔值到数字的隐式强制类型转换

在将某些复杂的布尔逻辑转换为数字加法的时候,隐式强制类型转换能派上大用场。

function onlyOne(a, b, c) {
  return !!((a && !b && !c) || (!a && b && !c) || (!a && !b && c))
}

如果其中有且仅有一个参数为 true,则 onlyOne(..) 返回 true。其在条件判断中使用了隐式强制类型转换,其他地方则是显式的,包括最后的返回值。

现在只接受三个参数,为了应对更多的参数可以使用从布尔值到数字(0 或 1)的强制类型转换:

function onlyOne() {
  var sum = 0
  for (var i = 0; i < arguments.length; i++) {
    // 跳过假值,和处理 0 一样,但是避免了 NaN
    if (arguments[i]) {
      sum += arguments[i]
    }
  }
  return sum == 1
}

通过 sum += arguments[i] 中的隐式强制类型转换,将真值(true/truthy)转换为 1 并进行累加。如果有且仅有一个参数为 true,则结果为 1;否则不等于 1,sum == 1 条件不成立。

# 隐式强制类型转换为布尔值

相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生布尔值隐式强制类型转换:

  • if (..) 语句中的条件判断表达式。
  • for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
  • while (..)do..while(..) 循环中的条件判断表达式。
  • ? : 中的条件判断表达式。
  • 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。

以上情况中,非布尔值会被隐式强制类型转换为布尔值,遵循前面介绍过的 ToBoolean 抽象操作规则。

值得一提的是,逻辑运算符 ||(或)和 &&(与)和其他语言不同,在 JavaScript 中它们返回的并不一定是布尔值,而是是两个操作数中的一个。

||&& 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件判断。

对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。

&& 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

# 宽松相等和严格相等

宽松相等(loose equals)== 和严格相等(strict equals)=== 都用来判断两个值是否“相等”,前者允许在相等比较中进行强制类型转换,而后者不允许。

ES5 规范 11.9.3 节的“抽象相等比较算法”定义了 == 运算符的行为,其中提到:

  • 如果两个值的类型相同,就仅比较它们是否相等(注:NaN 不等于 NaN,+0 等于 -0)。
  • 两个对象指向同一个值时即视为相等,不发生强制类型转换。
  • 比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或两者都转换为相同的类型后再进行比较。

字符串和数字之间的相等比较:

  • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
  • 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

其他类型和布尔类型之间的相等比较:

  • 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
  • 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
// 如果我们接下来要根据 a 是否是一个真值来做一些事情
var a = '42'

// 请不要这样用
// 因为根据规则 true 强制类型转换为 1,变成 1 == "42",
// 二者的类型仍然不同,"42" 根据规则被强制类型转换为 42,最后变成 1 == 42,结果为 false。
if (a == true) {
  // ..
}
// 也不要这样用
if (a === true) {
  // ..
}
// 这样的显式用法没问题
if (a) {
  // ..
}
// 这样的显式用法更好
if (!!a) {
  // ..
}
// 这样的显式用法也很好
if (Boolean(a)) {
  // ..
}

另外,nullundefined 之间的 == 也涉及隐式强制类型转换:

  • 如果 xnullyundefined,则结果为 true
  • 如果 xundefinedynull,则结果为 true

关于对象(对象/函数/数组)和标量基本类型(字符串/数字/布尔值)之间的相等比较:

  • 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
  • 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果。

# 比较少见的情况

如何让一个变量同时等于两个不同的数字?

var i = 2
Number.prototype.valueOf = function() {
  return i++
}
var a = new Number(42)

if (a == 2 && a == 3) {
  console.log('Yep, this happened.')
}

宽松相等中的隐式强制类型转换最为人诟病的地方是假值的相等比较:

'0' == null // false
'0' == undefined // false
'0' == false // true -- 晕
'0' == NaN // false
'0' == 0 // true
'0' == '' // false

false == null // false
false == undefined // false
false == NaN // false
false == 0 // true -- 晕
false == '' // true -- 晕
false == [] // true -- 晕
false == {} // false

'' == null // false
'' == undefined // false
'' == NaN // false
'' == 0 // true -- 晕
'' == [] // true -- 晕
'' == {} // false

0 == null // false
0 == undefined // false
0 == NaN // false
0 == [] // true -- 晕
0 == {} // false

# 极端情况

还有更极端的例子:

;[] == ![] // true

根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。所以 [] == ![] 变成了 [] == false

0 == '\n' // true

由于 """\n"(或者 " " 等其他空格组合)等空字符串被 ToNumber 强制类型转换为 0,所以等式成立。

为了避免这些难以琢磨的比较,我们应当遵循以下两个原则:

  • 如果两边的值中有 true 或者 false,千万不要使用 ==
  • 如果两边的值中有 []"" 或者 0,尽量不要使用 ==

隐式强制类型转换在部分情况下确实很危险,这时为了安全起见就要使用 ===

# 抽象关系比较

“抽象关系比较”(abstract relational comparison),分为两个部分:比较双方都是字符串(后半部分)和其他情况(前半部分)。

比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强制类型转换为数字来进行比较。

如果是两个普通对象,可能会优点奇怪:

var a = { b: 42 }
var b = { b: 43 }

a < b // false
a == b // false
a > b // false

a <= b // true
a >= b // true

根据规范 a <= b 被处理为 b < a,然后将结果反转。因为 b < a 的结果是 false,所以 a <= b 的结果是 true

如果比较双方都是字符串,则按字母顺序来进行比较:

var a = ['42']
var b = ['043']
a < b // false

因为 ToPrimitive 返回的是字符串,所以这里比较的是 "42""043" 两个字符串,它们分别以 "4""0" 开头。因为 "0" 在字母顺序上小于 "4",所以最后结果为 false。

相等比较有严格相等,关系比较却没有“严格关系比较”(strict relational comparison)。为了保证安全,应该对关系比较中的值进行显式强制类型转换。