[翻译]解释JavaScript中的类型转换
原文地址:JavaScript type coercion explained
类型转换是将值从一种类型转换为另一种类型的过程(比如字符串转换为数值,对象转换为布尔值,等等)。任何类型,无论是原始类型还是对象,都是类型强制的有效主体。回想下,js中的基本类型有:string, boolean, null, undefined , Symbol ( ES6中新增的)。
作为实际中类型强制的示例,请查看JavaScript比较表,这说明了松散的等式是如何实现的。==
运算符行为不同a
和b
类型。这个矩阵看起来很可怕,因为隐式强制==
运算符是这样做的,而且很难记住所有这些组合。你不必这么做,只要学习基本类型的强制原则就行了。
本文深入介绍了类型强制在JavaScript中的工作方式,并将为您提供必要的知识,以便您能够自信地解释以下表达式的计算结果。在这篇文章的结尾,我将给出答案并加以解释。
true + false 12 / "6" "number" + 15 + 3 15 + 3 + "number" [1] > null "foo" + + "bar" 'true' == true false == 'false' null == '' !!"false" == !!"true" [‘x’] == ‘x’ [] + null + 1 [1,2,3] == [1,2,3] {}+[]+{}+[1] !+[]+[]+![] new Date(0) - 0 new Date(0) + 0
是的,这个列表中充满了你作为开发人员可以做的非常愚蠢的事情。在90%的用例中,最好避免隐式强制。将此列表视为一项学习练习,以测试您对类型强制操作的知识。如果你觉得无聊,你可以找到更多的例子wtfjs.com.
顺便说一句,有时在JavaScript开发人员的面试中,您可能会遇到这样的问题。所以,继续读😄。
隐式转换 VS 显式转换
类型转换包含隐式和显式两种方式。
因为JavaScript是一门弱类型语言,所以值可能在不同类型之间自动转换,这种方式称为隐式转换。当你将运算符应用于不同类型的值的时候,隐式转换通常会出现:比如 1 == null
, 2/’5'
, null + new Date()
,有时候上下文环境也会触发因式转换:在if (xxx) {}中,xxx会被隐式转换为布尔值。
不会触发隐式转换的操作符是 === ,被称为严格相等操作符(全等)。相等操作符(==)会同时进行类型转换和比较操作。
隐式类型转换是一把双刃剑:它既是沮丧和缺陷的大部分来源,同时也是一种非常有用的机制(允许我们写更少量但是不会丢失可读性的代码)...
3种类型转换
首先我们需要知道的是在JavaScript中只有3中类型转换:
- to string
- to boolean
- to number
其次,基本类型和复杂类型(对象)的转换逻辑不通,但是只能通过这三种方式进行转换。
让我们先从基本类型开始说起。
字符串转换
String()函数将一个值显式的转换为字符串。在使用二元操作符 + 的时候,当任意一个操作数是字符串的时候,会触发隐式转换:
String(123) // 显式 123 + '' // 隐式
所有基本类型转换为字符串都会像你预期的那样:
String(123) // '123' String(-12.3) // '-12.3' String(null) // 'null' String(undefined) // 'undefined' String(true) // 'true' String(false) // 'false'
Symbol类型转换为字符串有点棘手,因为它没办法进行隐式转换,只能显式转换为字符串。
String(Symbol('my symbol')) // 'Symbol(my symbol)' '' + Symbol('my symbol') // TypeError is thrown
布尔值转换
使用Boolean()函数将值进行显式转换。隐式转换发生在逻辑上下文中,或由逻辑操作符触发(|| && !)。
Boolean(2) // explicit if (2) { ... } // implicit due to logical context !!2 // implicit due to logical operator 2 || 'hello' // implicit due to logical operator
注*: 逻辑操作符,比如 || 和 && 会在内部进行布尔转换,但是实际上返回操作数的原始值,即使他们不是boolean值
// returns number 123, instead of returning true // 'hello' and 123 are still coerced to boolean internally to calculate the expression let x = 'hello' && 123; // x === 123
Boolean('') // false Boolean(0) // false Boolean(-0) // false Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false Boolean(false) // false
列表中的所有值都不能转换为True(除了Object,function,array,date,用户自定的类型,等等)。Symbol转换的为Boolean是True。空的对象,数组也是True:
Boolean({}) // true Boolean([]) // true Boolean(Symbol()) // true !!Symbol() // true Boolean(function() {}) // true
数值转换
通过Number()方法进行显式转换。
隐式转换问题比较棘手,因为出发隐式转换的情况有很多种:
- 比较运算符(<, >, <=, >=)
- 位操作运算符(| & ^ ~)
- 算术运算符(- + * / %)注意当 + 操作符的操作数有一个为string的时候,不会触发数字隐式转换
- 一元操作符 +
- 相等操作符(== 和 !=)注意 == ,当两个操作数都为string的时候,不会触发数字隐式转换
Number('123') // explicit +'123' // implicit 123 != '456' // implicit 4 > '5' // implicit 5/null // implicit true | 0 // implicit
Number(null) // 0 Number(undefined) // NaN Number(true) // 1 Number(false) // 0 Number(" 12 ") // 12 Number("-12.34") // -12.34 Number("\n") // 0 Number(" 12s ") // NaN Number(123) // 123
当将字符串转换为数值时,js引擎会去掉前缀和后边的空格,\n, \t 字符,如果已经处理过的字符串不能表示一个数字则返回NaN。如果字符串为空,则返回0.
null和undefined的处理方式不同,null转换为0,undefine转为NaN。
Symbol既不能显示也不能隐式转换为数值。如果转换的话会抛出错误TypeError,而不像undefined转为为NaN一样。
Number(Symbol('my symbol')) // TypeError is thrown +Symbol('123') // TypeError is thrown
这里有两个特殊的规则需要记住:
- 当时 == 操作符应用于null或者undefined的时候,并没有触发数值转换。null只能等于(==)null或者undefined,不能等于(==)其他任何值
null == 0 // false, null is not converted to 0 null == null // true undefined == undefined // true null == undefined // true
- NaN不全等于任何值(也不等于自身)
if (value !== value) { console.log("we're dealing with NaN here") }
对象转换
到目前为止,我们已经研究了原始值的类型强制。这不是很令人兴奋。
涉及到对象时,当引擎遇到如下表达式[1] + [2,3]
首先,它需要将一个对象转换为一个基本值,然后将其转换为最终类型。仍然只有三种类型的转换:数值、字符串和布尔。
最简单的情况是布尔转换:任何非基本类型转换的值都是true,无论这个对象或者数组是否为空。
对象内部通过[[ToPrimitive]]方法转换位基本类型,这个方法同时负责数值和字符串的转换。
这里是一个[[ToPrimitive]]伪实现:
[[ToPrimitive]](input,preferredType) {/***/}方法的参数是输入值和输出类型两个(Number 或者 String),输出类型参数不是必须的。
数值转换和字符串转换都使用了输入值得两个方法:function valueOf() { [native code] }
和function toString() { [native code] }...这两个方法都由Object.prototype声明,对于任何派生类型都是用,如:Date,Array等
总的来说,[[ToPrimitive]]函数算法如下:
- 如果输入值类型已经是一个基本类型,直接返回
- 调用输入值的toString()方法,如果该方法返回基本类型,则返回
- 调用输入值的valueOf()方法,如果该方法返回基本类型,则返回
- 如果2,3返回的均不是基本类型,抛出TypeError错误
数值转换优先调用function valueOf() { [native code] }
(3)然后再执行function toString() { [native code] }
(2)。字符串转换恰恰相反:function toString() { [native code] }
(2)其次执行function valueOf() { [native code] }
(3).
大多数内置类型没有valueOf属性
,或者说没有方法function valueOf() { [native code] }直接返回
对象本身,因为它不是基本类型而被忽略了。那就是为什么数值和字符串转型的方式相同:都是调用String()函数。
在preferredType(第二个参数)的帮助下不同的操作符可以触发数值或者字符串转换。但是这里有两个例外:==操作符和二元操作符触发默认的转换模式(preferredType
未指定,或等于default
)。在这种情况下,大多数内置的类型转换默认值是数值类型,除了Date对象转为为字符串。
下面是一个Date类型转换的例子:
你可以覆盖掉默认的toString()
和valueOf()
方法来处理object到基本类型的转换。
ES6 Symbol.toPrimitive 方法
在ES5中你可以重写object到基本类型的转换方法:function toString() { [native code] }
和function valueOf() { [native code] }
方法。
但是在ES6中,你需要更彻底的替换内部的[[ToPrimitive]]方法来实现[Symbol.toPrimtive]
方法。
例子
有了以上讲到的理论基础,我们回到我们的例子中:
true + false // 1 12 / "6" // 2 "number" + 15 + 3 // 'number153' 15 + 3 + "number" // '18number' [1] > null // true "foo" + + "bar" // 'fooNaN' 'true' == true // false false == 'false' // false null == '' // false !!"false" == !!"true" // true ['x'] == 'x' // true [] + null + 1 // 'null1' [1,2,3] == [1,2,3] // false {}+[]+{}+[1] // '0[object Object]1' !+[]+[]+![] // 'truefalse' new Date(0) - 0 // 0 new Date(0) + 0 // 'Thu Jan 01 1970 02:00:00(EET)0'
下面我们可以解释每一个表达式的含义。
二元操作符+触发数值隐式转换(对于true和false)
true + false ==> 1 + 0 ==> 1
算术操作符/触发数值隐式转换(对于字符串‘6’)
12 / '6' ==> 12 / 6 ==>> 2
操作符+左到右依次执行,所以表达式“number” + 15先执行,因为左侧操作数是字符串,+操作符触发字符串隐式转换,拼接15位字符串,+3的操作与之一样,
“number” + 15 + 3 ==> "number15" + 3 ==> "number153"
表达式左侧+显示行,类型相同不需要转换,就是数值,下一步的+操作,同上边的提到的,触发了字符串的隐式转换。
15 + 3 + "number" ==> 18 + "number" ==> "18number"
比较运算符触发了数值的隐式转换(对于[1] 和 null),[1]先调用了toString()方法转为字符串,然后隐式转换为数值
[1] > null ==> '1' > 0 ==> 1 > 0 ==> true
一元操作符+比二元操作符+有更高的优先级,所有先执行一元操作符,+‘bar’先被计算,触发了数值的隐式转换,因为bar不是一个有效的数值,所以返回NaN,然后执行二元操作符+,同上转换为字符串拼接:
"foo" + + "bar" ==> "foo" + (+"bar") ==> "foo" + NaN ==> "fooNaN"
==操作符触发数值的隐式转换,所以‘true’被转换为NaN,true被转换为1
'true' == true ==> NaN == 1 ==> false false == 'false' ==> 0 == NaN ==> false
==操作符一般会触发数值隐式转换,但是也有例外,null只== null或者undefined,不==任何值。
null == '' ==> false
!!操作符同时转换 ‘true’和‘false’(触发boolean隐式转换),因为他们是非空字符串,所以都会转换为布尔值true,==操作符两边都是boolean类型,都不会触发类型转换(之前说的==触发数值隐式转换),直接去比较
!!"false" == !!"true" ==> true == true ==> true
==操作符对于数组触发数值隐式转换,数组的valueOf()方法返回数组本身,然后因为返回值不是基本类型而被忽略,进而调用数组的toString()方法返回‘x’
['x'] == 'x'
==> 'x' == 'x'
==> true
一元操作符+会触发数值隐式对于[],数组会调有valueOf()方法,因为然后因为返回值不是基本类型而被忽略,进而调用数组的toString()方法返回‘’空字符串 .
[] + null + 1 ==> '' + null + 1 ==> 'null' + 1 ==> 'null1'
逻辑操作符|| &&会触发布尔隐式转换(但是返回值是原始值,不是布尔值)
0 || "0" && {} ==> (0 || "0") && {} ==> (false || true) && true // internally ==> "0" && {} ==> true && true // internally ==> {}
不需要转换,因为都是数组类型,因为==检查对象(复杂类型)标识符,其实是内存地址(而不是两个对象相等),因为两个数组是两个不同实例,所以返回false
[1,2,3] == [1,2,3] ==> false
所有的操作数都不是基本类型,二元操作符+从左到右依次触发数值转换。
但是Object和Array的valueOf()方法返回自身,因此被忽略。toString()方法紧接着被做为回调。这里的技巧是:首先{}不是一个对象,在这里只是一个语法块(声明了一个语句块)。真正的开始是从一元操作符+开始,+[]调用toString()方法然后转为数值为0.
{}+[]+{}+[1] ==> +[]+{}+[1] ==> 0 + {} + [1] ==> 0 + '[object Object]' + [1] ==> '0[object Object]' + [1] ==> '0[object Object]' + '1' ==> '0[object Object]1'
通过操作符的优先级一步一步来最好解释了。
!+[]+[]+![] ==> (!+[]) + [] + (![]) ==> !0 + [] + false ==> true + [] + false ==> true + '' + false ==> 'truefalse'
二元操作符-触发了数值转换,Date类型的valueOf()方法返回自Unix时代的毫秒数
new Date(0) - 0
==> 0 - 0
==> 0
+操作符触发了对象的默认转换。Date类型是用string作为默认转换类型的,因此调用toString()方法而不是valueOf()
new Date(0) + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0'