JavaScript中的强制类型转换
1. 抽象值操作
1.1 ToString
抽象操作ToString
负责处理非字符串到字符串的强制类型转换。
基本类型值的字符串化规则为:null
转换为”null”,undefined
转换为”undefined”,true
转换为””true”。数字的字符串化遵循通用规则,那些极小和极大的数字使用指数形式:
1 | var a = 1.07*1000*1000*1000*1000*1000*1000*1000 |
对普通对象来说,除非自行定义,否则toString()
返回内部属性[[Class]]的值,如”[object Object]”。
数组的默认toString()
方法经过了重新定义,将所有单元字符串化以后再用”,”连接起来:
1 | var a = [1,2,3] |
1.2 ToNumber
抽象操作ToNumber
将非数字值转换为数字值。
其中true
转换为1,false
转换为0,undefined
转换为NaN
,null
转换为0。
ToNumber
对字符串的处理基本遵循数字常量的相关规则(字符串中含有非数字类型字符返回NaN
)。
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
1.3 ToPrimitive
抽象方法ToPrimitive
将对象值转换为相应的基本类型值。该方法会首先检查该值是否有valueOf()
方法,如果有并且返回基本类型值,就使用该值进行强制类型转换;如果没有就使用toString()
的返回值(如果存在)来进行强制类型转换;如果valueOf()
和toString()
均不返回基本类型值,会产生TypeError
错误。
从ES5
开始,使用Object.create(null)
创建的对象原型属性为null
,并且没有valueOf()
和toString()
方法,因此无法进行强制类型转换。
1.4 ToBoolean
抽象操作ToBoolean
将非布尔值转换为布尔值。
1.4.1 假值
假值的布尔强制类型转换结果为false
。
以下这些是假值:
- undefined
- null
- false
- +0、-0和NaN
- “”
从逻辑上说,假值列表以外的都应该是真值,但是JavaScript
规范对此并没有明确定义,只是给出了一些实例,例如规定所有的对象都是真值。
1.4.2 假值对象
例如:
1 | var a = new Boolean(false) |
a,b,c
都是封装了假值的对象,但是d
为true
,说明a、b、c
都为true
。因此假值对象并非封装了假值的对象。
假值对象看起来和普通对象并无二致,但将它们强制类型转换为布尔值时结果为false
。最常见的例子是document.all
,它是一个类数组对象,包含了页面上所有元素,它以前曾是一个真正意义上的对象,布尔强制类型转换结果为true
,不过现在它是一个假值对象。
1.4.3 真值
真值就是假值列表之外的值。真值列表可以无限长,无法一一列举,所以只能用假值列表作为参考。
2. 显示强制类型转换
2.1 字符串和数字之间的显示转换
字符串和数字之间的转换是通过String()
和Number()
这两个内建函数来实现的,请注意它们前面没有new
关键字,并不创建封装对象。
String()
遵循前面讲过的ToString
规则,将值转换为字符串基本类型。Number()
遵循前面讲过的ToNumber
规则,将值转换为数字基本类型。
除了String()
和Number()
以外,还有其他方法可以实现字符串和数字之间的显示转换:
1 | var a = 42 |
a.toString()
是显式的,不过其中涉及隐式转换。因为toString()
对42
这样的基本类型值并不适用,所以JavaScript
引擎会自动为42
创建一个封装对象,然后对该对象调用toString()
。
上例中+c
是+
运算符的一元形式(即只有一个操作数)。+
运算符显式地将c
转换为数字,而非数字加法运算(也不是字符串拼接)。
一元运算符 -
和 +
一样,并且还会反转数字的符号位。由于 --
会被当作递减运算符来处理,所以我们不能使用 --
来撤销反转,而应该像 - -"3.14"
这样,在中间加一个空格。
尽量不要把一元运算符 +
(还有 -
)和其他运算符放在一起使用。此外d = +c
也容易和d += c
搞混,两者天壤之别。
2.1.1 日期显示转换为数字
一元运算符 +
的另一个常见用途是将日期(Date
)对象强制类型转换为数字,返回结果为Unix
时间戳。
1 | var time = new Date() |
2.1.2 奇特的~运算符
~
首先将值强制类型转换为32
位数字,然后执行字位操作“非”(对每一个字位进行反转)。
字位反转是个很晦涩的主题,JavaScript
开发人员一般很少需要关心到字位级别。
对~
还可以有另外一种诠释:返回2的补码!
所以 ~x
大致等同于 -(x+1)
。
1 | ~42 //-(42+1) ==> -43 |
在-(x+1)
中唯一能够得到0
(或者严格说是-0
)的x
值是-1
,而在JavaScript
中有些函数用-1
来代表执行失败,用大于等于0
的值来代表函数执行成功。
比如,indexOf()
方法在字符串中搜索指定的字符串,如果找到就返回子字符串的位置,否则返回-1
。
1 | var a = "Hello World" |
~
和indexOf()
一起可以将结果强制类型转换为真/假值,如果indexOf()
返回-1,~
将其转换为假值0,其他情况一律转换为真值。
1 | var a = "Hello World" |
2.1.3 字位截除
~~x
能将值截除为一个32为整数,~~
中的的第一个~
执行ToInt32
并反转字位,然后第二个~
再进行一次字位反转,即将所有字位反转回原值,最后得到的仍然是ToInt32
的结果。
首先~~
只适用于32位数字,更重要的是它对负数的处理与Math.floor()
不同。
1 | Math.floor(-49.6) // -50 |
2.2 显式解析数字字符串
解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但是解析和转换两者之间还是有明显的差别。比如:
1 | var a = "42" |
解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败返回NaN
。
解析字符串中的浮点数可以使用parseFloat()
函数。从ES5
开始parseInt()
默认转换为十进制数,除非指定第二个参数作为基数。
不要忘了parseInt()
针对的是字符串,向parseInt()
传递数字和其他类型的参数是没有用的。非字符串会首先被强制类型转换为字符串,应该避免向parseInt()
传递非字符串参数。
1 | parseInt(1/0,19) //18 |
parseInt(1/0,19)
最后的结果是18,而非报错,因为parseInt(1/0,19)
实际上是parseInt("Infinity",19)
。基数19,它的有效数字字符范围是0-9
和a-i
(区分大小写),以19为基数时,第一个字符”I”值为18,而第二个字符”n”不是一个有效的数字字符,解析到此为止,和”42px”中”p”一样。
2.3 显式转换为布尔值
和前面讲过的+
类型,一元运算符!
显式地将值强制类型转换为布尔值。但是它同时还将真值反转为假值。所以显式强制类型转换为布尔值最常用地方法是!!
。
在if()
这样的布尔值上下文中,建议使用Boolean()
和!!
来进行显式转换以便让代码更清晰易读。
3. 隐式强制类型转换
3.1 字符串和数字之间的隐式强制类型转换
ES5
规范中定义:如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+
将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive
抽象操作,该抽象操作再调用[[DefaultValue]],以数字作为上下文。
简单来说就是,如果+的其中一个操作数是字符串(或者通过以上步骤可以得到字符串),那么就执行字符串拼接,否则执行数字加法。
1 | var a = [1,2] |
因为数组的valueOf()
操作无法得到简单基本类型值,于是调用toString()
,因此两个数组变成了"1,2"
和"3,4"
,+
将它们拼接后返回。
a + ""
(隐式)和前面的String(a)
(显式)之间有一个细微的差别需要注意。根据ToPrimitive
抽象操作规则,a + ""
会对a
调用valueOf()
方法,然后通过ToString
抽象操作将返回值转换为字符串,而String(a)
则是直接调用toString()
方法。
3.2 布尔值到数字的隐式强制类型转换
如果其中有且仅有一个参数为true
,则onlyOne()
返回true
。
1 | function onlyOne() { |
无论使用隐式转换还是显式转换,我们都可以通过修改onlyTwo()
或者onlyFive()
来处理更加复杂的情况,只需要将最后的条件判断从改为2或5。这比加入一大堆&&
和||
表达式要简洁得多。
3.3 隐式强制类型转换为布尔值
相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生布尔值隐式强制类型转换。
if()
语句中的条件判断表达式for(..; ..; ..)
语句中的条件判断表达式while()
和do .. while()
? :
中的条件判断表达式- 逻辑运算符
||
和&&
左边的操作数
3.4 ||和&&
其实不太赞同将它们称为“逻辑运算符”,因为这不太准确。称它们为“选择器运算符”或者“操作数选择器运算符”更恰当一些。
ES5
规范中说到:&&
和||
运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
对于||
来说,如果条件判断结果为true
就返回第一个操作数的值,如果为false
就返回第二个操作数的值。
对于&&
来说,如果条件判断结果为true
就返回第二个操作数的值,如果为false
就返回第一个操作数的值。
** 这里的条件判断结果指的是左边的操作数。
下面是一个十分常见的||
的用法,也许你已经用过但却并未完全理解:
1 | function foo(a,b) { |
再来看看&&
,有一种用法开发人员不常见,然而JavaScript
代码压缩工具常用。就是如果第一个操作数为真值,则&&
运算符选择第二个操作数作为返回值,这也叫做“守护运算符”,即前面的表达式为后面的表达式把关。
1 | function foo() { |
3.5 Symbol类型的强制类型转换
ES6
中引入了Symbol
类型,它的强制类型转换有一个坑。ES6
允许从Symbol
到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误,例如:
1 | var s1 = Symbol("cool") |
Symbol
类型不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式都是true
)。
4. 宽松相等和严格相等
常见的误区是“==
检查值是否相等,===
检查值和类型是否相等”,正确的解释是:“==
允许在相等比较中进行强制类型转换,而===
不允许”。事实上,==
和===
都会检查操作数的类型,区别在于操作数类型不同时它们的处理方式不同。
4.1 抽象相等
ES5
规范11.9.3节的“抽象相等比较算法”定义了==
运算符的行为。该算法简单而又全面,涵盖了所有可能出现的类型组合,以及它们进行强制类型转换的方式。
1 | 比较运算x==y, 其中x和 y是值,产生true或者false。这样的比较按如下方式进行: |
4.1.1 字符串和数字之间的相等比较
1 | var a = 42 |
a==b
是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。具体怎么转换?这就需要匹配前文的“抽象相等比较算法”,寻找适应的转换规则。
根据第4条规则返回x == ToNumber(y)
的结果。
4.1.2 其他类型和布尔类型之间的相等比较
==
最容易出错的一个地方是true
和false
与其他类型之间的相等比较。
1 | var a = '42' |
结果是false
,这让人很容易掉坑里。如果严格按照“抽象相等比较算法”,这个结果也就是意料之中的。根据第7条规则,若Type(y)
为Boolean
, 返回比较x == ToNumber(y)
的结果,即返回'42' == 1
,结果为false
。很奇怪吧?所以无论什么情况下都不要使用== true
和== false
。
4.1.3 null和undefined之间的相等比较
在==
中null
和undefined
相等,这也就是说在==
中null
和undefined
是一回事,可以相互进行隐式强制类型转换。
** 掌握“抽象相等比较算法”,读者可以自行推倒为什么[]==![]
返回true
。
4.2 比较少见的情况
1 | if(a == 2 && a == 3){ |
你也许觉得这不可能,因为a
不会同时等于2和3。但如果让a.valueOf()
每次调用都产生副作用,比如第一次返回2,第二次返回3,就会出现这样的情况。
1 | var i = 2 |
还有一个坑常常被提到:
1 | 0 == '\n' //true |
""
、"\n"
(或者" "
等其他空格组合)等空字符串被ToNumber强制类型转换为0。
4.3 完整性检查
再来看看那些“短”的地方:
1 | "0" == false // true |
其中有4种情况涉及== false
,之前我们说过应该避免,所以还剩下后面3种。
这些特殊情况会导致各种问题,使用中要多加小心。我们要对==
两边的值认真推敲,以下两个原则可以让我们有效地避免出错。
- 如果两边的值中有
true
或者false
,千万不要使用==
- 如果两边的值中有
[]、""、或者0
,尽量不要使用==
隐式强制转换在部分情况下确实很危险,为了安全起见就要使用===
5. 抽象关系比较
以 x
和 y
为值进行小于比较(x < y
的比较),会产生的结果可为true,false
或 undefined
(这说明 x
、y
中最少有一个操作数是 NaN
)。除了 x
和 y
,这个算法另外需要一个名为 LeftFirst
的布尔值标记作为参数。这个标记用于解析顺序的控制,因为操作数 x
和 y
在执行的时候会有潜在可见的副作用。LeftFirst
标志是必须的,因为 ECMAScript
规定了表达式是从左到右顺序执行的。LeftFirst
的默认值是 true
,这表明在相关的表达式中,参数 x
出现在参数 y
之前。如果 LeftFirst
值是 false
,情况会相反,操作数的执行必须是先 y
后 x
。这样的一个小于比较的执行步骤如下:
1 | 1. 如果 LeftFirst 标志是 true,那么 |
下面的例子就有些奇怪了:
1 | var a = {b:42} |
如果a < b
和a == b
结果为false
,为什么a <= b
和a >= b
的结果会是true
呢?
因为根据规范a <= b
被处理为b < a
,然后将结果反转。因为b < a
的结果为false
,所以a <= b
的结果为true
。
这可能与我们设想的大相径庭,即<=
应该是“小于或者等于”,实际上,JavaScript
中<=
是“不大于”的意思,即a <= b
被处理为 !(b < a)
。
** 规范设定NaN
既不大于也不小于任何其他值。
本文作者 : 冰比冰水冰
原文链接 : http://iceiceice.top/2018/06/01/forced-type-conversion/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!