无用但有趣的冷知识,通过[ ] ( ) ! + 构建 Javascript 世界,hope you enjoy it!

Javascript 是一门非常奇怪,同时也非常棒的语言,我们可以用它写出非常疯狂但却奏效的代码,同时,它也能根据我们使用的方式进行类型转换从而辅助开发。

如果将字符串(string)和其他类型参数相加,它会猜测我们需要文本格式,最后结果将返回 string 类型。

如果将其他类型参数加上 +- 前缀,它知道我们需要一个数值类型(Number),如果类型转换合法,紧接着就会将参数转换成数值类型。

如果将参数取反(!),它会把类型转换成布尔值。

下面让我们从最基础的开始,这里有几条黄金法则:

1.通过 ! 转换为布尔(Boolean)类型
2.通过 + 转换为数值(Number)类型
3.与 [] 相加转换为字符(String)类型

通过上面的黄金法则,让我们实践几个例子:

1
2
3
![] === false
+[] === 0
[]+[] === ""

当然,聪明的你肯定知道可以通过中括号加索引的方式从字符串中获取对应位置的字符:

1
"hello"[0] === "h"

通过多个数值字符相加,再转换为数值类型,即可获得多位数:

1
+("1" + "1") === 11

非常棒,通过上面的铺垫,现在我们已经拿到了字母 a

1
2
3
4
5
![] === false
![]+[] === "false"
+!![] === 1
------------------------
(![]+[])[+!![]] === "a" // same as "false"[1]

如此,通过一些简单的结合,我们还可以从单词 truefalse 中获取到 aeflrstu,那剩下的字母呢?

别忘了,我们还可以通过 [][[]] 这种方式获得 undefined,复用上面的方式,我们能拿到字母 din

1
[][[]] + [] === "undefined"

到目前为止,通过我们拿到的字母,已经可以拼出 fillfilterfind 这些单词,当然,你还可以组合成其他的单词,值得一提的是,上面提出三个单词都属于数组方法,这意味可以在数组实例中直接被调用,例如 [2,1].sort()

语法上,[2,1]["sort"]() 是与 [2,1].sort() 等价的。

让我们继续看看,使用字母拼凑的数组方法还能得到什么,目前我们还不需要执行这些函数:

1
[]["fill"]

上面的代码最终会产生 function fill() { [native code] },通过黄金法则,我们将结果再次转换为 String 类型:

1
[]["fill"]+[] === "function fill() { [native code] }"

现在,我们又获得了 cov(){}(空格)。

通过新获取的 co,现在可以组合单词 constructorconstructor 是 JS 对象中返回构造函数的方法,下面就让我们通过 constructor 从已有对象类型中获取对应字符串形式的构造函数:

1
2
3
4
true["constructor"] + [] === "function Boolean() { [native code] }"
0["constructor"] + [] === "function Number() { [native code] }"
""["constructor"] + [] === "function String() { [native code] }"
[]["constructor"] + [] === "function Array() { [native code] }"

通过这些构造函数,字母集合中增添了 BNSAmgy

现在可以构建 toString,同样,可以通过中括号使用的函数,不过这次我们要执行它:

1
(10)["toString"]() === "10"

但前文中我们已经通过第三条黄金法则熟练地将任何类型转换为字符串类型了,toString 的存在看起来有点鸡肋,没用了?

别忘了,数值类型的 toString 方法还有第二个参数 radixradix 决定了数值转换为字符串类型前被转换为的进制,举个例子:

1
2
3
4
(12)["toString"](10) === "12" // base 10 - normal to us
(12)["toString"](2) === "1100" // base 2, or binary, for 12
(12)["toString"](8) === "14" // base 8 (octonary) for 12
(12)["toString"](16) === "c" // hex for 12

机智的你肯定想到了,为什么只写到 16 进制?进制最大可以是 36,这可包括了 0-9a-z 中的所有字母,现在我们可以拿到我们想要的任何字母:

1
2
(10)["toString"](36) === "a"
(35)["toString"](36) === "z"

太棒了,我们已经拿到了全部小写字母,但新问题摆在眼前,标点符号和大写字母该怎么办呢?

根据 JS 执行的位置,它可能有权限访问特定的预定义对象或数据,如果是在浏览器中运行,那么就可以有访问到一些传统的 HTML 包装方法,例如,bold 是一个字符串方法,可以将字符串用 <b> 标签包裹。

1
"test"["bold"]() === "<b>test</b>"

这样,我们就拿到了 <>/

你可能在项目开发中使用过 escape 函数,它可以将字符串转换为浏览器可以翻译的 URI 友好格式,这个函数在我们接下来的工作中扮演了重要角色,我们需要用到它。通过拼凑字母得到这个单词,但问题是如何使其执行,它是一个全局函数,不属于任何类型。

那么函数的构造函数是什么呢?其实就是函数对象本身,function Function() { [native code] }

1
[]["fill"]["constructor"] === Function

通过 Function,我们可以传入字符串来构建一个函数:

1
Function("alert('test')");

运行得到:

1
2
3
Function anonymous() {
alert('test')
}

我们只需要在末尾加上 () 就可以立即执行这个函数,如你所见,我们现在可以真正执行代码了!

小试牛刀,运行 escape 函数:

1
[]["fill"]["constructor"]("return escape(' ')")() === "%20"

如果我们给 escape 函数传入 <,会得到 %C,如果想获得盛夏的大写字母,这个 C 至关重要。

1
[]["fill"]["constructor"]("return escape('<')")()[2] === "C"

通过 C,我们可以得到 fromCharCode 函数,通过给定的十进制参数,可以得到对应的 Unicode 字符,它属于字符对象,因此调用方式可以参照前文:

1
2
""["constructor"]["fromCharCode"](65) === "A"
""["constructor"]["fromCharCode"](46) === "."

通过 Unicode 速查可以快速找到任何字符对应的数值。

到这里,Javascript 世界的构建元素已经全部找齐!我们已经能拿到我们需要的任何参数,并将它们连接到一起形成代码并执行,这意味,我们仅通过 []()+! 实现了 Javascript 的图灵完备

想证明一下?不妨在浏览器控制台里执行下面的代码:

如果你是在手机上看的,可以告诉你,上面执行的是 alert("wtf")

jsFuck 可以自动转换你的代码,这里是过程介绍。

你说了这么多,有用吗?

如果你非要问我有没有用,我只能说点儿用也没,不过 eBay 前段时间出了个 Bug,网站里允许卖家在页面中插入这些字符构成的 JS 代码,但这种攻击媒介不是很常见。有人说可以用来进行代码混淆,实际上,有更好的混淆方式。

所以,开心就好~~

原文:A Javascript journey with only six characters