先来看一个奇怪的JS字符串:

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

不要疑惑,这是JS编程(包括NodeJS)中可以正常输出的字符串。

挑战不可能:只用6个字符,写JS程序_第1张图片

如果alert或console出来,你猜运行结果是什么?

console.log( (![]+[])[+!+[]+!+[]+!+[]]+(![]+[])[+!+[]+!+[]+!+[]+!+[]]+(![]+[])[+!+[]+!+[]]+(![]+[])[+[]] );

会得到的输出是:self

挑战不可能:只用6个字符,写JS程序_第2张图片

再用这段代码:

挑战不可能:只用6个字符,写JS程序_第3张图片

很神奇是不是,这到底是怎么回事呢?

这段字符中,只用到了:[]()!+,这6个字符。为什么得到的是:self!?

基本原理

我们之所以能够抛开其他字符不用,要归功于JS的类型系统和数据类型转换机制。

这 6 个字符是这样各显神通的:[]可以用来创建数组,!和+可以在数组上执行一些操作,再用()给这些操作分组。

先看一个简单的数组:

[]

数组前加上!会把它转成布尔值。

数组被认为是真值,因此取非之后变成了false:

![] === false

在JS中,除非转换为相同类型,否则无法将不同类型的值加在一起。

JS在进行转换时遵循着这个规则,而且如果进行相加,JS会对某些特殊内容,如true进行自动转换类型:

在表达式2 + true中,JS会将true转成数字,得到表达式2+1。

在表达式2 + "2"中,JS会将数字转成字符串,得到2 + "2" === "22"。

接下来,好戏马上来了。

JavaScript 数组强制转换

JS数组相加,会转换成字符串,再并连接起来。

空数组转换为空字符串,因此将两个空数组相加将得到空字符串。

[] + [] === "" + "" === ""

数组跟其他类型值相加时也一样:

![] + [] === "false" + "" === "false"

惊不惊喜?我们得到了目标字符串"self"所包含的几个字符!

再使用数组下标,就可以按正确的顺序提取所需的字符:

"false"[3] === "s"

(![] + [])[3] === "s"

那么,如何生成数字呢?

生成数字

前面提到过:可以把数组转成布尔值。

那如果用加号+把它转成数字会怎样?

+[] === ???

JS会尝试调用数组的valueOf方法,但是发现不存在这个方法,然后就转而调用toString()方法了。

因此上面的代码等效于:

+[] === +""

将字符串转换为数字将产生以下结果:

+"42" === 42
+"esg" == NaN
+"" === 0

空字符串是一个 false值,跟 null,undefined和数字零类似,因此将其中任何一个转换为数字都会变成零:

+null === 0
+undefined === 0
+false === 0
+NaN === 0
+"" === 0

因此,将数组转换为数字需要先将其转换为字符串,最后转成 0:

+[] === +"" === 0

第一个数字已经造出来了!我们还需要更多数字,继续:

!0 === !false
!false === true
!0 === true

将 0 取否就得到一个为真的布尔值。为真的布尔值转成数字,就是1:

+true === 1

有了1,自然就可以得到2。

用上面的转换大法,可以轻松得到我们想要的这些数字:

1 === +true == +(!0) ==== +(!(+[])) === +!+[]

1 === +!+[]
2 === +!+[] +!+[]
3 === +!+[] +!+[] +!+[]
4 === +!+[] +!+[] +!+[] +!+[]

临门一脚,大功告成

总结下这些规则:

数组属于真值,取否就得到 false: ![] // false

数组相加时会转换成字符:[] + [] // ""

空数组转成数字得到 0,再去否得到 true,再转成数字得到1:+(!(+[])) === 1

根据这些规则,我们就能得到想要的字符串。看下面这个示意图就很清楚了:

![] + [] === "false"
+!+[] === 1

(![] + [])[3] + (![] + [])[4] + (![] + [])[2] + (![] + [])[0]
^^^^^^^^^^      ^^^^^^^^^^      ^^^^^^^^^^      ^^^^^^^^^^      
 "false"         "false"         "false"         "false"      

^^^^^^^^^^^^^   ^^^^^^^^^^^^^   ^^^^^^^^^^^^^   ^^^^^^^^^^^^^    
     s               e               l               f        

最终的表达式就是这样:

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

整理下空格和换行,就是一行代码,就是本文最初的字符串:

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

知道了这个原理,你可以写出很有意思的代码了。

再加以扩展,可以做出一些更有意思的事情。

还有一种应用场景:JS代码加密。

当然这并不是像 JShaman 一样的专业JS代码混淆加密。

但也可以做为一种另类的代码保护方式了。