有一个著名的jsfuck的网站,可以使用6个字符来实现所有的js代码。例如 alert(1) 就可以用以下片段来实现。
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[
]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]
])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+
(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+
!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![
]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]
+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[
+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!!
[]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![
]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[
]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![
]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(!
[]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])
[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(
!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[
])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])()
这里面其实蕴含了JS里的强制转换。
抽象值操作
在介绍强制类型转换之前,我们先介绍下抽象操作。基本类型中,null会转换成‘null’,undefined会转换成‘undefined’, 对于普通对象来说toString()返回的是[object Object],如果有自己的toString方法,就会调用该方法。 有时我们需要把非数字值转换成数字使用,这时有抽象操作ToNumber。其中true转换成为1,false转换成0,undefined转换成NaN,null转换成0。
ToBoolean
这里我们只需要记住只有少数几种情况会返回假值。
- undefined
- null
- false
- +0,-0,NaN
- ''
字符串与数字的显式转换
我们可以用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]]。 简单来说就是,如果+的其中一个操作数是字符串(或者可以通过以上步骤可以得到字符串),执行字符串拼接,否则执行数字加法。
隐式强制转换成布尔值
if(..)
for(..;..;..)
里的条件判断表达式while(..)``do.. while(..)
?.. :..
|| &&
以上情况中,非布尔值都会被隐式强制转换成布尔值。规则与上面介绍的一致。
学习了前两点,我们可以试着解释一下![]+[]
会发生什么了。首先是![]
会隐式强制转换成布尔值,结果就是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]到底是怎么来的呢?这就要详细介绍一下这部分了。 有三种情况会转换成原始值
- 转换为布尔值,完全不用担心,即使是空对象空数组结果也是true
- 遇到对象相减或者应用数学函数,这时会转换为数字
- 字符串转换,会遇到在alert()或者类似上下文中
hint
对象的转换有三种变体 "string" "number" "default", 当我们对象与+计算时,就会选择default hint,还有与数字,字符串Symbol == 比较时,也会选择default。
具体转换方法
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
- 调用
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键Symbol.toPrimitive
(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是
"string"
—— 尝试调用obj.toString()
或obj.valueOf()
,无论哪个存在。优先调用toString()
方法,如果不是原始类型再调用valueOf()
- 否则,如果 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的设计理念。