[翻译]解释JavaScript中的类型转换

  原文地址:JavaScript type coercion explained

  类型转换是将值从一种类型转换为另一种类型的过程(比如字符串转换为数值,对象转换为布尔值,等等)。任何类型,无论是原始类型还是对象,都是类型强制的有效主体。回想下,js中的基本类型有:string, boolean, null, undefined , Symbol ( ES6中新增的)。

  作为实际中类型强制的示例,请查看JavaScript比较表,这说明了松散的等式是如何实现的。==运算符行为不同ab类型。这个矩阵看起来很可怕,因为隐式强制==运算符是这样做的,而且很难记住所有这些组合。你不必这么做,只要学习基本类型的强制原则就行了。

  本文深入介绍了类型强制在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 == null2/’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

  这里有两个特殊的规则需要记住:

  1.   当时 == 操作符应用于null或者undefined的时候,并没有触发数值转换。null只能等于(==)null或者undefined,不能等于(==)其他任何值
null == 0               // false, null is not converted to 0
null == null            // true
undefined == undefined  // true
null == undefined       // true 
  1.   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]]函数算法如下:

  1. 如果输入值类型已经是一个基本类型,直接返回
  2. 调用输入值的toString()方法,如果该方法返回基本类型,则返回
  3. 调用输入值的valueOf()方法,如果该方法返回基本类型,则返回
  4. 如果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'

 

posted @ 2019-07-28 14:01  韩帅  阅读(270)  评论(0编辑  收藏  举报