JavaScript里的强制转换

29 阅读5分钟

有一个著名的jsfuck的网站,可以使用6个字符来实现所有的js代码。例如 alert(1) 就可以用以下片段来实现。

[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[
]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]
])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+
(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+
!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![
]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]
+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[
+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!!
[]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![
]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[
]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![
]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(!
[]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])
[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(
!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[
])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])()

这里面其实蕴含了JS里的强制转换。

抽象值操作

在介绍强制类型转换之前,我们先介绍下抽象操作。基本类型中,null会转换成‘null’,undefined会转换成‘undefined’, 对于普通对象来说toString()返回的是[object Object],如果有自己的toString方法,就会调用该方法。 有时我们需要把非数字值转换成数字使用,这时有抽象操作ToNumber。其中true转换成为1,false转换成0,undefined转换成NaN,null转换成0。

ToBoolean

这里我们只需要记住只有少数几种情况会返回假值。

  1. undefined
  2. null
  3. false
  4. +0,-0,NaN
  5. ''

字符串与数字的显式转换

我们可以用String() Number()来进行显式强制转换,注意不用加new关键字。转换的结果遵循前面的ToString() ToNUmber(), +是一个一元运算符,一般认为是显式强制转换 +还可以用来获取当前的时间戳

const timestamp = +new Date()

显式转换为布尔值

使用Boolean()可以显式的强制转换成布尔值。也可以使用!!

隐式强制类型转换

+里的隐式转换

我们来看一个例子

var a = [1,2];
var b = [3,4];
a+b

这个结果是“1,23,4” 大概的原理是+运算中,如果某个操作数是字符串或者能够通过以下操作转换成字符串,+进行拼接操作。对象和数组会先调用Toprimitive抽象操作,再调用[[DefaultValue]]。 简单来说就是,如果+的其中一个操作数是字符串(或者可以通过以上步骤可以得到字符串),执行字符串拼接,否则执行数字加法。

隐式强制转换成布尔值

  1. if(..)
  2. for(..;..;..)里的条件判断表达式
  3. while(..)``do.. while(..)
  4. ?.. :..
  5. || &&

以上情况中,非布尔值都会被隐式强制转换成布尔值。规则与上面介绍的一致。

学习了前两点,我们可以试着解释一下![]+[]会发生什么了。首先是![]会隐式强制转换成布尔值,结果就是false,false + []结局就是false。注意了,实际开发中,要尽量避免这种写法,因为可读性很糟糕,但是还是建议作为科普了解一下原理。

宽松相等

宽松相等即是==。其中有两个例外,一个是NaN不等于NaN,另一个是+0等于-0。

字符串与数字比较

42==‘42’会发生什么呢?根据ES5规范,会将字符串转换成数字,再进行比较。

其他类型和布尔值比较

这时会把布尔类型转成数字进行比较,例如true==1结果是true null==undefined,这两个数据类型会算作相等

对象和非对象比较

这时会将对象采用ToPrimitive操作再与数字或字符串比较。例如

var a = 42;
var b = [42];
a == b //true

对象与原始值的转换

一个object要怎么变成原始值呢?我们经常在报错界面看到[object object]到底是怎么来的呢?这就要详细介绍一下这部分了。 有三种情况会转换成原始值

  1. 转换为布尔值,完全不用担心,即使是空对象空数组结果也是true
  2. 遇到对象相减或者应用数学函数,这时会转换为数字
  3. 字符串转换,会遇到在alert()或者类似上下文中

hint

对象的转换有三种变体 "string" "number" "default", 当我们对象与+计算时,就会选择default hint,还有与数字,字符串Symbol == 比较时,也会选择default。

具体转换方法

为了进行转换,JavaScript 尝试查找并调用三个对象方法:

  1. 调用 obj[Symbol.toPrimitive](hint) —— 带有 symbol 键 Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话,
  2. 否则,如果 hint 是 "string" —— 尝试调用 obj.toString() 或 obj.valueOf(),无论哪个存在。优先调用toString()方法,如果不是原始类型再调用valueOf()
  3. 否则,如果 hint 是 "number" 或 "default" —— 尝试调用 obj.valueOf() 或 obj.toString(),无论哪个存在。优先调用valueOf()方法,如果不是原始类型再调用toString()

这时候我们再来看42==[42],发生了什么呢?首先是数组会调用default hint的原始值转换,先调用valueOf()再调用toString(),valueOf()返回的是本身,所以忽略,再调用toString,就变成‘42’了,这时候在比较,字符串会再变成数字进行比较了。 一般的对象valueOf()返回的是自身,toString()结果是[object object]。所以报错的时候看到这个东西就不奇怪了。

结论

这次主要介绍了一些类型转换的规则。实际开发中比较常见的类型就是!!a 来强制转换成布尔值,+Date()来强制转换成时间戳。还有|| && if(..)等。宽松相等的法则其实比较罕见,日常工作中应当尽量避免使用==,一律使用===,也要注意NaN可能会导致===也会失灵。另外还有对象转换成基本值的基本原理。总体而言,只要掌握基本类型转换的规则,就不会觉得JavaScript出人意料了。复杂的原理可能工作中不会遇到,但是掌握一下知识,有助于理解整体JavaScript的设计理念。