前端静径

玩转js类型转换

前言

今天再深入探究一个js基础知识——类型转换。我们从一个问题开始讲解:

1
(!(~+[])+{})[--[~+""][+[]]*[~+[]] + ~~!+[]]+({}+[])[[~!+[]]*~+[]]

大家可以试着将上面的代码粘贴到控制台,看看输出的是什么?请输完之后再看后面的内容。

我的天,出来的居然是”sb”!咳咳,如果以后你对谁不爽了,直接甩这堆符号过去吧。

言归正传,咱们来探究下为什么会输出”sb”吧。

“sb”是如何炼成的

其实这没有看上去那么深奥,也就是利用了一个很简单但又很强大的js的类型转化的原理。如果把js类型转化的原理搞清楚了,这些个符号真的不在话下。希望看完下面的讲解,你们能设法写出个”nb”来~

下面我们将这里用到的概念都先讲解一下

js运算符的优先级

下图列出了 JavaScript 运算符,并按优先级顺序从高到低排列。具有相同优先级的运算符按从左至右的顺序计算。

alt 运算符优先级

可以根据上图将上面的那段代码进行分解:

alt 运算符优先级

js类型转换

js的类型转换大致从两个方面讲:

  1. 操作符转换(比如一些单目运算:+, ++, –, if语句)
  2. 等号转换(比如:==, ===, >=, <…)。

上面的问题涉及到的是第一种转换。

原始值到原始值的转换

  1. 原始值转换为布尔值: undefined、null、0、-0、NaN、””被转为false;其他所有原始值都被转为true。
  2. 原始值转换成字符串:直接转换成字符串
  3. 原始值转换为数字:
    • 布尔值转数字:true转换为1,false转换为0;
    • 字符串转数字:空格被忽略;以数字表示的字符串可以直接转为数字,其他字符不会转为数字。

原始值对到对象的转换

null和undefined无法自动转换为对象,抛出TypeError异常。其他类型调用相应的构造函数,转为包装对象:字符串调用String()、数字调用Number()、布尔值调用Boolean()。

对象到原始值的转换

  1. 对象到布尔值:所有的对象都转为true
  2. 对象到字符串:如果对象有toString()方法,且调用toString()返回原始值,则调用toString(),并将返回的原始值转为字符串。否则,如果对象有valueOf()方法,且调用valueOf()返回原始值,则调用valueOf(),并将返回的原始值转为字符串。否则,抛出类型错误异常
  3. 对象到数字:如果对象有valueOf()方法,且调用valueOf()返回原始值,则调用valueOf(),并将返回到原始值转为数字。否则,如果对象有toString()方法,且调用toString()返回原始值,则调用toString(),并将返回到原始值转为数字。否则,抛出类型错误异常
    • toString()的转换规则:
      • 数组类:每个元素转为一个字符串,并使用逗号连接。
      • 函数类:通常是,将函数转换为JavaScript源代码字符串。
      • 日期类:转换为一个日期和时间字符串(可被JavaScript解析)。
      • RegExp类:转换为正则表达式直接量的字符串。
    • valueOf()的转换规则:如果对象可以被转换为原始值,则转为原始值。否则,不做任何转换,返回对象本身。

等号转换

严格操作符===没有什么好讲的,也比较简单,以lRef == rRef为例,只要Type(lRef)Type(rRef)不同,那么总是会返回false。否则,对象必须指向相同的对象引用,字符串必须包含相同的字符,其他原始类型必须拥有相同的值。

另外,nullundefined永远不会===除自己以外其他类型,而NaN不会与任何类型===,甚至包括自己。

==操作符的规则如下(还是以lRef == rRef为例):

Type(x) Type(y) 结果
x与y同类型 - 结果同严格判断操作符(===)
null Undefined true
Undefined null true
Number String x == toNumber(y)
String Number toNumber(x) == y
Boolean (any) toNumber(x) == y
(any) Boolean x == toNumber(y)
String or Number Object x == toPrimitive(y)
Object String or Number toPrimitive(x) == y
otherwise… - false

如果算法的结果是一个表达式,那么它将被重新计算,直到最后得到一个PrimitiveBoolean值。
其中ToNumberToPrimitiveToBoolean一样,为抽象方法,规则如下:

  1. ToNumber
参数类型 结果
Undefined NaN
Null +0
Boolean 如果为true,返回1,否则返回0
Number 返回自身
String 与调用Number(string)结果一致: “abc” -> NaN, “123” -> 123
Object 会执行以下步骤:1. 让primValue = ToPrimitive(input argument, hint Number); 2. 返回ToNumber(primValue)
  1. ToPrimitive
参数类型 结果
Object (在判等操作符的场景下)如果input.valueOf()返回一个原始类型(primitive),直接返回input.valueOf();否则,如果input.toString()返回一个原始类型,那么返回input.toString(); 否则报错。
otherwise… 返回自身

规则都列出来了,那么下面咱们来分析下这段代码吧!

代码解析

我们一步步拆解这条语句,为了方便大家查找,我这里再贴一下代码:

1
(!(~+[])+{})[--[~+""][+[]]*[~+[]] + ~~!+[]]+({}+[])[[~!+[]]*~+[]]

先看这段(!(~+[])+{})

按照优先级,我们先执行+[], []是一个对象类型,前面是个单目运算符+,那么[]肯定是要转换成Number类型了。根据前面提到的:

对象到数字:如果对象有valueOf()方法,且调用valueOf()返回原始值,则调用valueOf(),并将返回到原始值转为数字。否则,如果对象有toString()方法,且调用toString()返回原始值,则调用toString(),并将返回到原始值转为数字。否则,抛出类型错误异常

因此首先调用[].valueOf()方法,数组调用valueOf()方法会返回自身,仍旧是个数组,因此不是原始值。所以就要调用toString()方法,根据前面提到的数组类toString()方法的转换规则:

每个元素转为一个字符串,并使用逗号连接。

因此,[].toString()会返回一个空串"",是原始值,于是调用Number("")返回数字0。至此,整个转换过程结束。+[]转换成数字0。接下来是~,它是位运算符,作用可以记为把数字取负然后减一,所以~0就是-1。也就是说(~+[])得到了-1,然后取非,得到false,至此,!(~+[])的结果是false。式子也就变成false + {},一个布尔值加上一个对象,那么对象{}先转化成原始类型,流程如下:

  1. 调用toPrimitive,发现是object类型
  2. 调用valueOf,返回自身{}
  3. 不是原始类型,调用toString,返回[object Object]
  4. false[object Object]相加,false先转化为字符串false
  5. 相加得结果false[object Object]

因此,整个(!(~+[])+{})返回的结果就是(false[object Object])

再看[–[~+””][+[]]*[~+[]] + ~~!+[]]

先看~+"",这个很简单,因为""之前有个加号+,因此最终目的也是将""转换成number。而空串转换成number会变成0,因此+""的结果就是0,然后~0的结果就是-1。再看+[],这个之前分析过了就不再赘述,其结果就是0,同样的~+[]的结果就是-1~~!+[]可以转换成~~!0,而!0的结果是true,前面加个~,会将true转换成number,也就是1,然后~1的值是-2,再对-2进行~操作,就变成了1。所以上式就变成了[--[-1][0]*[-1]+1]。按照运算符的优先级,先执行[-1][0],得到结果是-1,然后执行--操作,得到-2,然后执行-2*[-1],这里[-1]会倾向于转换成number类型,根据前面对象转初始值的转换规则,不难推出,其最终转换成-1,因此-2*(-1)变成2,再加1,变成3,至此--[~+""][+[]]*[~+[]] + ~~!+[]的结果就是3

整合(!(~+[])+{})[–[~+””][+[]]*[~+[]] + ~~!+[]]

经过前面的分析,不难得到,(!(~+[])+{})[--[~+""][+[]]*[~+[]] + ~~!+[]]经过一系列的转换,最终是这样子的式子: 'false[object Object]'[3],相信大家应该看出来了吧,就是取字符串'false[object Object]'的第三个位置的字符,最后得到的当然是s啦。至此sbs已经推导结束。

({}+[])[[~!+[]]*~+[]]

相信经过上面的分析,这段也不难了。
({}+[])转换成'[object Object]',[~!+[]]*~+[]的转换过程如下:

  1. ~!+[]变成-2
  2. ~+[]变成-1
  3. [-2]* (-1)结果是2

因此({}+[])[[~!+[]]*~+[]]也就是'[object Object]'[2],当然就是b啦。

‘s’和’b’相加,不就是’sb’么?

总结

前面总结了js运算符的优先级和js类型转换的一些原理。可以通过下面这两个题目来巩固下:

1
2
3
4
5
6
var a = [0];
if([0]){
console.log(a == true);
}else{
console.log("wut");
}

猜猜看这段代码会输出什么?

1
([][[]]+[])[+!![]]+([]+{})[!+[]+!![]]

我不介意你把这段的结果写在评论里面的(坏笑脸)