木匣子

Web/Game/Programming/Life etc.

心形表白代码

情人节的时候,在推特上看到了一个很神奇的代码,整个代码构成了几颗心的形状。既然是代码,当然可以运行咯。将其复制到浏览器的 console ,执行后弹出对话框:I love you.

真是太酷了有木有,通篇只用了 $=~[];{_+,:!"\. 这几个符号,连数字都没有出现,居然可以实现文字弹窗!

于是二话不说立马将它剖析,看看葫芦里面到底卖的什么药。其实剖析的另一个目的就是为了给妹子定制一个特殊的版本啦~

Step I. Format

这样漂亮的代码可没法读,剖析的第一步当然是对其进行格式化(format/beautify),将其打回原形!使用 Sublime Text 的 jsFormat 插件,或者 WebStorm 很容易即可完成格式化。当然,再手动调整一下就更好了:

$ = ~[];
$ = {
  ___:  ++$,
  $$$$: (![] + "")[$],
  __$:  ++$,
  $_$_: (![] + "")[$],
  _$_:  ++$,
  $_$$: ({} + "")[$],
  $$_$: ($[$] + "")[$],
  _$$:  ++$,
  $$$_: (!"" + "")[$],
  $__:  ++$,
  $_$:  ++$,
  $$__: ({} + "")[$],
  $$_:  ++$,
  $$$:  ++$,
  $___: ++$,
  $__$: ++$
};

$.$_ =
  ($.$_ = $ + "")[$.$_$] +
  ($._$ = $.$_[$.__$]) +
  ($.$$ = ($.$ + "")[$.__$]) +
  ((!$) + "")[$._$$] +
  ($.__ = $.$_[$.$$_]) +
  ($.$ = (!"" + "")[$.__$]) +
  ($._ = (!"" + "")[$._$_]) +
  $.$_[$.$_$] +
  $.__ +
  $._$ +
  $.$;

$.$$ =
  $.$ +
  (!"" + "")[$._$$] +
  $.__ +
  $._ +
  $.$ +
  $.$$;

$.$ =
  ($.___)[$.$_][$.$_];

$.$(
  $.$(
    $.$$ +
    "\"" +
    $.$_$_ +
    (![] + "")[$._$_] +
    $.$$$_ +
    "\\" +
    $.__$ +
    $.$$_ +
    $._$_ +
    $.__ +
    "(\\\"\\" +
    $.__$ +
    $.__$ +
    $.__$ +
    "\\" +
    $.$__ +
    $.___ +
    (![] + "")[$._$_] +
    $._$ +
    "\\" +
    $.__$ +
    $.$$_ +
    $.$$_ +
    $.$$$_ +
    "\\" +
    $.$__ +
    $.___ +
    "\\" +
    $.__$ +
    $.$$$ +
    $.__$ +
    $._$ +
    $._ +
    ".\\\"\\" +
    $.$__ +
    $.___ +
    ")" +
    "\""
  )()
)();

从格式化后的代码,很容易可以看出主要的五个部分。接下来就要对其一一分析了。

Step II. Parse

part 1

$ = ~[];                // $ = -1 
$ = {
  ___:++$,              // $.___  = $ = 0
  $$$$: (![] + "")[$],  // $.$$$$ = "false"[0]           = "f" 
  __$: ++$,             // $.__   = $ = 1
  $_$_: (![] + "")[$],  // $.$_$_ = "false"[1]           = "a"
  _$_: ++$,             // $._$_  = $ = 2
  $_$$: ({} + "")[$],   // $.$_$$ = "[object Object]"[2] = "b" 
  $$_$:($[$] + "")[$],  // $.$$_$ = "undefined"[2]       = "d" 
  _$$:++$,              // $._$$  = $ = 3
  $$$_: (!"" + "")[$],  // $.$$$_ = "true"[3]            = "e"
  $__:++$,              // $.$__  = $ = 4
  $_$: ++$,             // $.$_$  = $ = 5
  $$__: ({} + "")[$],   // $.$$__ = "[object Object]"[5] = "c" 
  $$_: ++$,             // $.$$_  = $ = 6
  $$$: ++$,             // $.$$$  = $ = 7
  $___: ++$,            // $.$___ = $ = 8
  $__$: ++$             // $.$__$ = $ = 9
};

这一部分的目的主要是构造出数字表 0~9 其中第一句利用 ~[] 得到 -1

为什么 ~[] 能得到 -1 呢,[]是一个具体对象,对应的布尔值是 true ,在 javascript 里 true 对应的数字是 1 。而 ~(NOT)运算 的具体运算过程是将数字按位取反后减1,所以 ~[] = ~1 = 0-1 = -1
更多关于 javascript 位运算的信息,可以看这里

然后 $ 从 -1 不断自加,得到 0~9, 忽略一些不相关的语句,我们可以看到:

$ = {
  ___:  ++$,            // $.___  = $ = 0
  __$:  ++$,            // $.__   = $ = 1
  _$_:  ++$,            // $._$_  = $ = 2
  _$$:  ++$,            // $._$$  = $ = 3
  $__:  ++$,            // $.$__  = $ = 4
  $_$:  ++$,            // $.$_$  = $ = 5
  $$_:  ++$,            // $.$$_  = $ = 6
  $$$:  ++$,            // $.$$$  = $ = 7
  $___: ++$,            // $.$___ = $ = 8
  $__$: ++$             // $.$__$ = $ = 9
};

此外这里还用 _ 表示 0; $ 表示 1 为变量合理命名,使得后面的引用变更有章可循。

_ _ _ _ 表示 0000; _ _ _ $ 表示 0001; _ _ $ _ 表示 0010;

part 2

$.$_ = 
  ($.$_ = $ + "")[$.$_$] +      // $.$_ = "[object Object]"
                                // "[object Object]"[5]        = "c"
  ($._$ = $.$_[$.__$]) +        // $._$ = "[object Object]"[1] = "o"
  ($.$$ = ($.$ + "")[$.__$]) +  // $.$$ = "undefined"[1]       = "n"
  ((!$) + "")[$._$$] +          // "false"[3]                  = "s"
  ($.__ = $.$_[$.$$_]) +        // $.__ = "[object Object]"[6] = "t"
  ($.$ = (!"" + "")[$.__$]) +   // $.$  = "true"[1]            = "r"
  ($._ = (!"" + "")[$._$_]) +   // $._  = "true"[2]            = "u"
  $.$_[$.$_$] +                 // "[object Object]"[5]        = "c"
  $.__ +                        //                               "t"
  $._$ +                        //                               "o"
  $.$;                          //                               "r"

第二部分做了一件了不起的事,有了数字序列后,如何得到字母?可能你会想到 ascii 之类的东西,但现实有点残酷,写这个代码的人不想用到 $=~[];{_+,:!"\. 之外的字符,当然包括 String.fromCharCode() 这样的函数,其实现在甚至连函数都无法执行。但是他利用了 javascript 的类型转换特性:

当一个 object 与字符串相加的时候,会自动调用object的 toString() 方法,而 object 默认返回 "[object Object]" 于是 ($ + "")[$.$_$] 实际上就得到了 "[object Object]"[5] = "c"

真是太聪明了。用类似的方法可以得到 “undefined”、“true” 和 “false” 。最重要的一点,我们凑齐了 "constructor" 这个单词!

part 3

$.$$ = 
  $.$ +                 //             "r"
  (!"" + "")[$._$$] +   // "true"[3] = "e"
  $.__ +                //             "t"
  $._ +                 //             "u"
  $.$ +                 //             "r"
  $.$$;                 //             "n"

这里用同样的方法得到了 "return" 是不是感觉得到函数的气息了。

part 4

$.$ = 
  ($.___)[$.$_][$.$_];  // (0)["constructor"]["constructor"]

Wow! 数字的构造函数,不就是 Number() 么; Number() 的构造函数,矛头直指 Function() 啊!正是如此,这里漂亮地拿下了函数构造函数,也就是说,我们可以用 Function() 来创造自定义函数了。

Function(“alert(‘hello world!’)”); 相当于创建一个匿名函数 function(){ alert(‘hello world!’); }

part 5

$.$(                    // Function(
  $.$(                  //   Function(
    $.$$ +              //     "return
    "\"" +              //     "\""
    $.$_$_ +            //     "a"
    (![] + "")[$._$_] + //     "l"
    $.$$$_ +            //     "e"
    "\\" +              //     "\\"
    $.__$ +             //     "1"
    $.$$_ +             //     "6"
    $._$_ +             //     "2"
    $.__ +              //     "t"
    "(\\\"\\" +         //     "(\\\"\\"
    $.__$ +             //     "1"
    $.__$ +             //     "1"
    $.__$ +             //     "1"
    "\\" +              //     "\\"
    $.$__ +             //     "4"
    $.___ +             //     "0"
    (![] + "")[$._$_] + //     "l"
    $._$ +              //     "o"
    "\\" +              //     "\\"
    $.__$ +             //     "1"
    $.$$_ +             //     "6"
    $.$$_ +             //     "6"
    $.$$$_ +            //     "e"
    "\\" +              //     "\\"
    $.$__ +             //     "4"
    $.___ +             //     "0"
    "\\" +              //     "\\"
    $.__$ +             //     "1"
    $.$$$ +             //     "7"
    $.__$ +             //     "1"
    $._$ +              //     "o"
    $._ +               //     "u"
    ".\\\"\\" +         //     ".\\\"\\""
    $.$__ +             //     "4"
    $.___ +             //     "0"
    ")" +               //     ")"
    "\""                //     "\""
  )()                   //   )()
)();                    // )();

连函数构造函数都有了,万事具备只欠东风,接下来只要描述出想要执行的函数体就可以了。但是手头上只有 $=~[];{_+,:!"\. 这几个字符可以怎么办!?这时候就要请 ascii 大驾光临了。

在 javascript 的字符串中,可以使用 \xxx 的八进制来表示单个 ascii 字符。可以在这里查看 ascii 对应的八进制。

但是在现有条件下,我们只能先构造出能表达 ascii 的字符串,而不是函数体。

假设 var a=1,b=6,c=7,我们得先得到 "\\"+a+b+c = "\167" 然后才得到字符 "a"

所以这里不得不嵌套调用 Function,在里面一层组装函数体的 ascii 码,然后运行后得到真正的函数体,在外面一层运行。

Function(
    Function(
        "return(\"+
            /* function body ascii code */
        +"\")"
    )()
)();

至于函数体的构造,如果量不大的话,手工查表完成也不是难事,如果量大的话,可以写一个辅助程序来完成转换工作。于是我写了这么一个东西:

var code = "alert(\"My secret!\")";
var ret = "";
for (var i=0, len=code.length; i<len; i++) {
    ret += "\\"+code.charCodeAt(i).toString(8);
}
ret = ret.replace(/./g, function(c){
    return {
        "\\": "\"\\\\\"+",
        "0": "$.____+",
        "1": "$.___$+",
        "2": "$.__$_+",
        "3": "$.__$$+",
        "4": "$._$__+",
        "5": "$._$_$+",
        "6": "$._$$_+",
        "7": "$._$$$+",
        "8": "$.$___+",
        "9": "$.$__$+"
    }[c];
});

// output
console.log(ret);

Step 3. Reshape

现在代码分析完了,知道它的工作原理了。但是要怎么把它变回原来的心形呢?

我想到了一个很高科技的算法,首先去除代码中的间隔,分析可伸缩字符位置,然后动态构造心形,最后采用填充算法将代码填充进去,当然得保证代码最终可以运行……

即使是这样,我对如何实现它完全没有思路,看来只能靠纯体力活完成了。好再自己还是有点艺术细胞的,为了妹子,豁出去了!

最后,实际上排版工作只花了我半个多小时,把 1367 个字符拼成了3颗心~效果展示