WTFJS | 聊一聊JS中的隐式类型转换

327 阅读7分钟

xxx.png

有趣的例子

先来看几个有趣的例子

[]等于 ![]

[] == ![]; // -> true

true不等于[], 也不等于 ![]

true == []; // -> false
true == ![]; // -> false   ???

❓ null & false

!!null; // -> false
null == false; // -> false  ???

[] & {}

console.log([] + {});   ???

上述问题截取自 wtfjs ( 即What the f*ck JavaScript? ) ,以下是其对于JavaScript的部分描述。

JavaScript is quite a funny language with tricky parts. Some of them can quickly turn our everyday job into hell, and some of them can make us laugh out loud.

尤其是对于js中的类型转换相关问题,可以说是非常贴切了。该仓库中列出的大部分tricky parts均是与类型转换相关的,更具体地说应该是隐式类型转换,这也是我为什么想写这篇文章的原因。

网上有很多关于类型转换的文章,文末也列出了一些参考文章。

如果你能熟练的得出上述问题的答案,说明你对于JavaScript中的隐式类型转换已经挺熟悉了,如果对此不是很熟悉,那么也没关系,接下来我们一起来梳理一下JavaScript中的隐式类型转换。

前置知识

什么是ToPrimitive?

规范中的解释在这里:9.1 ToPrimitive

以下文字出自冴羽大佬的JavaScript深入之头疼的类型转换(下)

函数语法表示如下:

ToPrimitive(input[, PreferredType])

第一个参数是 input,表示要处理的输入值。

第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String。

当不传入 PreferredType 时,如果 input 是日期类型,相当于传入 String,否则,都相当于传入 Number。

如果传入的 input 是 Undefined、Null、Boolean、Number、String 类型,直接返回该值。

如果是 ToPrimitive(obj, Number),处理步骤如下:

  1. 如果 obj 为 基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

规范怎么说?

以下是涉及隐式类型转换的一些常见类型

1. 一元操作符 +

ES5规范1.4.6 简单的说就是会调用toNumber方法去处理,具体结果如下

类型结果
UndefinedNaN
Null+0
Booleantrue >>> 1; false >>> 0
Number直接返回
String具体见下方具体实例
Object先调用ToPrimitive得到primValue,然后返回ToNumber(primValue)

string 转数字

console.log(Number()) // +0console.log(Number(undefined)) // NaN
console.log(Number(null)) // +0console.log(Number(false)) // +0
console.log(Number(true)) // 1console.log(Number("123")) // 123
console.log(Number("-123")) // -123
console.log(Number("1.2")) // 1.2
console.log(Number("0x11")) // 17
console.log(Number("")) // 0
console.log(Number(" ")) // 0
console.log(Number("123 123")) // NaN
console.log(Number("foo")) // NaN
console.log(Number("100a")) // NaNconsole.log(Number("1.2.2")) // NaN
console.log(Number(".2")) // 0.2

可以看到toNumber(string)有如下特点:

  • 尝试将传输值转换为整数或浮点数
  • 忽略前导0
  • 出现不是数字的字符,返回NaN (遇到的第一个.会被当做小数点处理)

2. 二元操作符 +

以 x + y 为例

  1. prim_x = ToPrimitive(x)
  2. prim_y= ToPrimitive(y)
  3. prim_x或者prim_y其中一个为字符串,返回 ToString(prim_x)ToString(prim_y)的拼接结果
  4. 返回 ToNumber(prim_x)ToNumber(prim_y)的运算结果
console.log(null + 1);
console.log([] + []);  //相当于两个空字符串拼接
console.log([] + {});  // 相当于 "" + "[object Object]"
console.log({} + []);  // 相当于 "[object Object]" + ""

PS: {} + [] 直接在chromes控制台运行结果是0,因为第一个{}被当作一个空代码块,所以这段代码在这种情况下相当于+[], 也就是toNumber([]),故返回 0。

如果({} + [])这样直接运行的话就会和console的方式得到相同的结果。

那这样的话在控制台中直接运行{} +{},是不是得0呢?

并不会。会得到'[object Object][object Object]'

一些在**WTFJS**中出现的一些例子:

// baNaNa
"b" + "a" + +"a" + "a"; // -> 'baNaNa'
"foo" + +"bar"; // -> 'fooNaN'// Adding arrays
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'// Math with true and false
true + true; // -> 2
(true + true) * (true + true) - true; // -> 3// Funny math
 3  - 1  // -> 2
 3  + 1  // -> 4
'3' - 1  // -> 2
'3' + 1  // -> '31'
'' + '' // -> ''
[] + [] // -> ''
{} + [] // -> 0
[] + {} // -> '[object Object]'
{} + {} // -> '[object Object][object Object]''222' - -'111' // -> 333// 这种形式,用猜的方式也能直到需要toNumber
[4] * [4]       // -> 16
[] * []         // -> 0
[4, 4] * [4, 4] // NaN

3. ==

11.9.3 The Abstract Equality Comparison Algorithm

以下截取了规范中关于 The Abstract Equality Comparison Algorithm 的相关描述,嫌太长可以跳过,看总结部分。

  1. If Type(x) is the same as Type(y), then

    1. If Type(x) is Undefined, return true.

    2. If Type(x) is Null, return true.

    3. If Type(x) is Number, then

      1. If x is NaN, return false.
      2. If y is NaN, return false.
      3. If x is the same Number value as y, return true.
      4. If x is +0 and y is 0 , return true.
      5. If x is 0 and y is +0, return true.
      6. Return false.
    4. If Type(x) is String, then return true if x and y are exactly the same sequence of characters (same length and same characters in corresponding positions). Otherwise, return false.

    5. If Type(x) is Boolean, return true if x and y are both true or both false. Otherwise, return false.

    6. Return true if x and y refer to the same object. Otherwise, return false.

  2. If x is null and y is undefined, return true.

  3. If x is undefined and y is null, return true.

  4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y).

  5. If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.

  6. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.

  7. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).

  8. If Type(x) is either String or Number and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).

  9. If Type(x) is Object and Type(y) is either String or Number, return the result of the comparison ToPrimitive(x) == y.

  10. Return false.

总结( x == y)

  • 如果xy类型相同的话,分为以下几种情况:

    • 都是undefined或者都是null,返回true

    • 都是Number

      • 其中一方是NaN,返回false
      • 数值相同,返回true
      • +0 和 -0 比较, 返回true
  • 如果x与y类型不同的话

    • null 和 undefined ,对应2、3

       console.log(null == undefined); //true
      
    • 字符串与数字比较, 对应4、5

      都转换为数字进行比较

    • x或者y其中一方为布尔量, 对应6、7

      console.log(true == '2')
      
    • x或者y一方为对象,另一方为字符串或者数字

      若x为对象,ToPrimitive(x) == y

      若y为对象,x == ToPrimitive(y)

    一些在**WTFJS**中出现的一些例子:

// NaN is not a NaN
NaN === NaN; // -> false
​
​
// [] is equal ![]
[] == ![]; // -> true// true is not equal ![], but not equal [] too
true == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> true// null is falsy, but not false
!!null; // -> false
null == false; // -> false
​
​
// true is false
!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true// [] is truthy, but not true
!![]       // -> true
[] == true // -> false// Array equality is a monster
[] == ''   // -> true
[] == 0    // -> true
[''] == '' // -> true
[0] == 0   // -> true
[0] == ''  // -> false
[''] == 0  // -> true
​
[null] == ''      // true
[null] == 0       // true
[undefined] == '' // true
[undefined] == 0  // true
​
[[]] == 0  // true
[[]] == '' // true
​
[[[[[[]]]]]] == '' // true
[[[[[[]]]]]] == 0  // true
​
[[[[[[ null ]]]]]] == 0  // true
[[[[[[ null ]]]]]] == '' // true
​
[[[[[[ undefined ]]]]]] == 0  // true
[[[[[[ undefined ]]]]]] == '' // true

20230415更新
突然发现一个小游戏,关于==符号,类似扫雷游戏。 eqeq.js.org/

image.png

其他

这一部分列举了其他WTFJS中一些其他的案例,有兴趣的可以看看

  • It's a fail
// It's a fail
(![] + [])[+[]] +
  (![] + [])[+!+[]] +
  ([![]] + [][[]])[+!+[] + [+[]]] +
  (![] + [])[!+[] + !+[]];
// -> 'fail'

第一眼看到这一长串式子,心里:💢 MD这都啥跟啥呀!!!💢 , 但是如果认真看过上面提到的知识,耐心一点是能够得到这个fail结果的。

具体过程这里就不在赘述,It's a fail 中有详细的过程解释。

  • null 与 0
null == 0; // false  //1
null > 0; // false   //2
null >= 0; // true   //3

首先看第一个,不要先入为主的将null 转换为0 再进行比较,仔细回想一下,会发现之前规范中并没有这样的规则说要在这里把null转换为0, 刚刚与预想的情形根本不会出现。

如果在 == 两边出现了一个null, 那么只有另一边也是null 或者是 undefined的情况下,返回值财位 true, 对应**11.9.3 The Abstract Equality Comparison Algorithm**中2、3.

接下来将2 和 3 放在一起看,null 不大于0 , 同时null 又大于等于0 ❓❓❓

对于2, 判断的流程应该如下:

null > 0
+null = +0
0 > 0
false

而对于null >= 0, 可能会猜想出现这种情况:null > 0 || null == 0, 但这样会返回false,所以很明显不是。原因在于 >=以一种不同的方式运作,详见11.8.4 The Greater-than-or-equal Operator ( >= ) 。简单地说就是 当 null >= 0进行比较时,依据是 null < 0的结果,如果 null < 0为true, 那么返回 false; 反之返回true

除开上述提到的一些部分,WTFJS中还有一些其他实例,有兴趣可以去查看,另外其中的中文翻译文档貌似有些错误,直接看英文版就好。

参考:

WTFJS

JavaScript 深入之头疼的类型转换(上)

JavaScript深入之头疼的类型转换(下)

Javascript : The Curious Case of Null >= 0

ECMAScript 5.1