仓库源文站点原文


title: JavaScript 类型转换的有趣应用 layout: post categories: JavaScript tags: 类型转换 表达式

excerpt: 介绍 js 中类型转换机制的一个有趣应用

背景

可以访问这个网站提前预览:https://knightyun.github.io/magic-expression/

先来看一串代码:

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

也许你在其他地方看见过这种黑科技操作,那么不妨猜一下上面的代码的值等于多少,实在猜不到可以复制它粘贴到浏览器 console 中回车看看;

这里剧透一下,会得到下面的结果(手动解释,仅供娱乐并无其他意思):

"sb"

当然,你可能还见过这种形式的 "hello world!" 版本的,即最后输出的字符串是 "hello world!",这里由于它的代码过长就不粘贴出来了,原理和上面类似,我们暂且称它为魔法表达式,下面就以上面的例子还分析一下这窜代码的“魔法”;

类型转换

分析之前先来了解一些基础,在 js 的世界中,存在一种类型转换的机制,大致就是字面意思,下面分别举例说明;

Number / String

console.log(1 + 1); // 2
console.log('1' + '1'); // "11"
console.log(1 + '1'); // "11"
console.log('1' + 1); // "11"

console.log(1 - 1); // 0
console.log('1' - '1'); // 0
console.log(1 - '1'); // 0
console.log('1' - 1); // 0

console.log('a' - 'b'); // NaN
console.log('a' - '1'); // NaN
console.log('a' - 1); // NaN
// *,/,% 等运算符与 - 运算类似

可以看到,上面的行为可能有点怪异,因为在其他语言中可能会报错,但是在 js 中确实是这样执行的,即静默地尽可能地把运算符两边的类型转换一致再运算;

Number / Boolean

当然这种转换不限于字符串和数字类型之间,也包括其他类型,比如很经典的一中转换:

console.log(1 == true); // true
console.log(1 === true); // false

上面就是把数字类型和 Boolean 类型的值进行比较,第一行的输出结果是因为使用 == 操作符时会自行把 1 转换为 true,所以两边相等(可以理解为执行 Boolean(0) 操作);而 === 操作符除了比较两边的值,还会比较两边类型,二者都相同才判断相等,即没有进行类型转换;

其他类型

其他类型的转换情况:

console.log(Boolean([])); // true
console.log(Boolean('')); // false
console.log(Boolean(String(''))); // false
console.log(Boolean(new String(''))); // true
console.log(Boolean(' ')); // true
console.log(Boolean({})); // true
console.log(Boolean(0)); // false
console.log(Boolean(Number(0))); // false
console.log(Boolean(new Number(0))); // true

应用

下面是该机制的一些实际应用:

console.log(+'1', typeof +'1'); // 1 "number"
console.log(1 + '', typeof (1 + '')); // 1 "number"
console.log('' + 1, typeof ('' + 1)); // 1 "string"

console.log(+[], typeof +[]); // 0 "number"
console.log(-[], typeof -[]); // -0 "number"
console.log(+[1], typeof +[1]); // 1 "number"
console.log(+[1,2], typeof +[1,2]); // NaN "number"
console.log('' + [], typeof ('' + []), ('' + []).length); // '' "string" 0
console.log([] + '', typeof ('' + []), ('' + []).length); // '' "string" 0

console.log(+{}, typeof +{}); // NaN "number"
console.log({} + '', typeof ({} + '')); // [object Object] "string"
console.log('' + {}, typeof ('' + {})); // [object Object] "string"

另外,"!""~" 运算符也算是 js 中较为常见的,其中 ! 是逻辑运算符,代表,而 ~ 是位运算符,代表按位取反,它们有以下关系:

console.log(!true); // false
console.log(!false); // true
console.log(!!true); // true

console.log(~0); // -1
console.log(~3); // -4
console.log(~~3); // 3

分析

现在开始分析最早提到的那串代码,它的神奇之处就在于整个代码在不包括任何一个字母的情况下输出了字母,代码中都是一些运算符和操作符,代码串挨在一起不利于观察,我们先稍微格式化一下:

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

+

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

这里只是拆分美化了一下格式,输出结果不变,然后一行一行进行分析,根据拆分结果,其实整个代码就是两大部分相加;

第一部分

第一部分中,第一行是 (!(~+[]) + {}),也是两部分加和,根据前面的基础,可以得到如下分析结果:

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

            ↓

(!(~0) + "[object Object]")

            ↓

 (!-1 + "[object Object]")

            ↓

(false + "[object Object]")

            ↓

(false + "[object Object]")

            ↓

  ("false[object Object]")

第一大部分剩下的内容:

[
    --[~+''] [+[]] * 
    [~+[]] + 
    ~~!+[]
]

分析结果如下:

--[~+''][+[]]  *  [~+[]]   +  ~~!+[]

      ↓             ↓           ↓

  --[~0][0]    *   [~0]    +  ~~!0

      ↓             ↓           ↓

  --[-1][0]    *   [-1]    + ~~true

      ↓                         ↓

   --(-1)                      ~~1

      ↓                         ↓

     -2                         1

=> -2 * [-1] + 1 = -2 * -1 + 1
                 = 3

所以第一大部分的结果是:

("false[object Object]")[3]; // "s"

第二部分

第二大部分也执行类似的分解,第一行内容是 ({} + []),其实也是在拼接字符串,分析如下:

      ({} + [])

          ↓

("[object Object]" + "")

          ↓

  ("[object Object]")

余下内容是:

[
    [~!+[]] *
    ~+[]
];

分析如下:

 [~!+[]]  *  ~+[]

    ↓         ↓

  [~!0]   *   ~0

    ↓         ↓

 [~true]  *   -1

    ↓         ↓

   [~1]   *   -1

    ↓         ↓

   [-2]   *   -1

         ↓

      -2 * -1

         ↓

         2

所以第二大部分结果就是:

("[object Object]")[2]; // "b"

汇总

最后两个大部分内容字符串拼接就是最终结果,这里汇总一下:

( !(~+[]) + {} ) // "false[object Object]"
[
    --[~+''] [+[]] * // -2
    [~+[]] + // -1
    ~~!+[] // 1
] // => ("false[object Object]")[3] => "s"

+

({} + []) // [object Object]
[
    [~!+[]] * // -2
    ~+[] // -1
]; // => ("[object Object]")[2] => "b"

// => "s" + "b" = "sb"

费了这么大功夫就得出两个字母,想必这个过程对于理解 js 中的类型转换机制是很有帮助的;

魔力所在

然后就是回顾之前那个问题,为什么整个代码没有出现字母却在结果中出现了,根据上面的分析可以看出,字符串是通过类似以下方式得到的:

console.log([] + {}); // "[object Object]"
console.log([] + true); // "true"
console.log([] + false); // "false"

然后在 js 中字符串也可以通过类似数组的方式获取某个字符:

console.log(("[object Objact]")[2]); // "b"
// "b" 在字符串中的索引为 2

那么前面提到的输出 hello world! 的代码,其实也是通过类似的方式获取字符然后拼接而成,只是需要思考从哪些格式化输出中获取想要的那个字符而已;

拓展

顺着上面的思路,如果我们想要输出任意指定字符,该如何实现呢?即字符中可能包含 a-z, A-Z, 0-9 中的任何一个字符,甚至是特殊字符;

可能的输出

前面提到输出的关键是存在这个一个标准格式化输出(如 true, false),然后就能从里面扣取字符了,作者目前能想到的标准输出的字符串有如下(可能疏漏):

console.log([]+[]); // ""
console.log([]+!![]); // "true"
console.log([]+![]); // "false"
console.log([]+{}); // "[object Object]"
console.log([]+!![]-[]); // "NaN"
console.log([]+[][+[]]); // "undefined"
console.log([]+~~!![]/+[]) // "Infinity"
console.log(([]+~[])[~~[]]); // "-"

即使这样,汇总下来的字母也只有:

abcdefiIjlnNoOrstuy-

离目标似乎有点远~~,数字就比较好弄了:

console.log(+[]); // 0
console.log(+!![]); // 1
console.log(!![]+!![]); // 2(后续的数字可以这样叠加)
console.log(-~+!![]); // 2(也可以换个简短的方法)
console.log(!![]+!![]+!![]); // 3
console.log(!![]+!![]+!![]+!![]); // 4
console.log(!![]+!![]+!![]+!![]+!![]); // 5
console.log(!![]+!![]+!![]+!![]+!![]+!![]); // 6
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 7
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 8
console.log(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // 9

更大的数字就可以通过字符拼接或者四则运算获得;

扩大输出范围

目前还是没有获得大部分大写字母和特殊字符 ~!@#$%^&*()_+-=\|[]{};':",./<>?,甚至是汉字或者是其他国字符,等等,说到这里是不是想起了什么?没错就是Unicode(号称万国符来着),首先 js 中使用 Unicode 的形式是 "\uXXXX",后面的 XXXX 是四个十六进制字符,例如:

console.log('\u0061'); // "a"

// 获取 a 的字符编码
console.log('a'.charCodeAt()); // 97

// 转换为 16 进制
console.log('a'.charCodeAt().toString(16)); // "61"
// 0061 = 61

// 汉字
console.log('黄'.charCodeAt().toString(16)); // "9ec4"
console.log('\u9ec4'); // "黄"

所以只要能够表示 \, u, a-f, 0-9 这几个字符,就能表示所有 Unicode 字符了!根据前面的总结,其实我们已经能够表示这几个字符了!不过呢,直接拼接 Unicode 的话会出现下面的问题:

console.log('\u0061'); // "a"
console.log('\u' + '0061'); // SyntaxError: Invalid Unicode escape sequence
console.log('\\u' + '0061'); // "\u0061"

eval

所以我们不能通过直接拼接 Unicode 字符串来获得能被解析的 Unicode 符的,因此不得不换个思路;既然不能拼接直接 Unicode,那么有么有间接的方法或者 API 呢? 可能会想到使用 eval()

var s = eval('"' + '\\u' + '0061' + '"');
console.log(s); // "a"

虽然成功了,但是目前我们似乎还无法获得 eval 中的字母 v,所以要继续换个方法;

Function

其实还有一个函数可以实现类似的功能,它就是 Function(),即构造函数的函数,也是声明函数的另一种方法,举例:

var fn = Function('a', 'b', 'return a + b');
var fn2 = Function('return 4');

console.log(fn(1, 2)); // 3
console.log(fn2()); // 4

然后我们就可以像这样拼接 Unicode 了:

console.log(Function('return ' + '"\\u' + '0061' + '"')());
// "a"

另外我们需要知道的是:[]['constructor']['constructor'] === Function,所以最后只要构造出这样的字符串就行了:

[]['constructor']['constructor']('return '+'"'+'\\u0061'+'"')();

根据前面的基础,我们已经能够获取 constructor, return 里面的所有字母了,这里再把需要用到的字符全部汇总一下:

([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]; // " ""
([]+/\\\\/)[+!![]]; // "\"
[]+(+[]); // "0"
[]+(+!![]); // "1"
[]+(!![]+!![]); // "2"
[]+(!![]+!![]+!![]); // "3"
[]+(!![]+!![]+!![]+!![]); // "4"
[]+(!![]+!![]+!![]+!![]+!![]); // "5"
[]+(!![]+!![]+!![]+!![]+!![]+!![]); // "6"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "7"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "8"
[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]); // "9"
([]+![])[+!![]]; // "a"
([]+{})[!![]+!![]]; // "b"
([]+{})[!![]+!![]+!![]+!![]+!![]]; // "c"
([]+[][+[]])[!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]]; // "d"
([]+!![])[!![]+!![]+!![]]; // "e"
([]+![])[+[]]; // "f"
([]+[][+[]])[+!![]]; // "n"
([]+{})[+!![]]; // "o"
([]+!![])[+!![]]; // "r"
([]+![])[!![]+!![]+!![]]; // "s"
([]+!![])[+[]]; // "t"
([]+!![])[!![]+!![]; // "u"

结果拼接

最后剩下的就是把想要的输出,转换为 Unicode,再拆分为单个字符对应上面的表达式进行拼接就行了,我们来试一下效果(拿走不谢 :)):

var s1 = 
[][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]](([]+!![])[+!![]]+([]+!![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[!![]+!![]]+([]+!![])[+!![]]+([]+[][+[]])[+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+'"'+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+[])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+![])[+[]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+[])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![])+([]+![])[+[]]+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![]+!![]+!![]+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(+[])+[]+(+[])+[]+(!![]+!![])+[]+(+!![])+'"')();

console.log(s1);
// "I love you!"

var s2 = 
[][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]][([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([]+[][+[]])[+!![]]+([]+![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[+!![]]+([]+!![])[!![]+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]]+([]+!![])[+[]]+([]+{})[+!![]]+([]+!![])[+!![]]](([]+!![])[+!![]]+([]+!![])[!![]+!![]+!![]]+([]+!![])[+[]]+([]+!![])[!![]+!![]]+([]+!![])[+!![]]+([]+[][+[]])[+!![]]+([]+{})[!![]+!![]+!![]+!![]+!![]+!![]+!![]]+'"'+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![])+[]+(+!![])+[]+(+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![]+!![]+!![]+!![])+[]+(!![]+!![])+[]+(!![]+!![]+!![])+[]+(+!![])+([]+/\\/)[+!![]]+([]+!![])[!![]+!![]]+[]+(!![]+!![]+!![]+!![])+([]+![])[+[]]+[]+(!![]+!![]+!![]+!![]+!![]+!![])+[]+(+[])+'"')();

console.log(s2);
// "我爱你"

在线转换

这里放一个在线转换的网站: 点我在线转换