翻译 | 一行 JavaScript 代码的逆向工程
- 原文地址:Reverse Engineering One Line of JavaScript
- 原文作者:Alex Kras
- 译者:李波
- 校对者:冬青、小萝卜
几个月前,我看到一个邮件问:有没有人可以解析这一行 JavaScript 代码
<pre id=p><script>n=setInterval("for(n+=7,i=k,P='p.\\n';i-=1/k;P+=P[i%2?(i%2*j-j+n/k^j)&1:2])j=k/i;p.innerHTML=P",k=64)</script>
这一行代码会被渲染成下图的效果。你可以在这里用浏览器打开来观看。这是 Mathieu ‘p01’ Henri 写的,你还可以在作者的网站www.p01.org里看到更多很酷的例子。
好的!我决定接受挑战
第一步:让代码变得可读
第一件事,让 HTML 文件里只有 HTML 代码,然后把 JavaScript 代码放到 code.js
文件里。我还用 id="p"
来包装 pre 标签。
index.html
<script src="code.js"></script>
<pre id="p"></pre>
我注意到变量 k
只是一个常量,所以把它移出来,然后重命名为 delay
。
code.js
var delay = 64;
var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var n = setInterval(draw, delay);
接下来,因为 setInterval
可以接收一个函数或者字符串来执行,字符串 var draw
会被 setInterval 用 eval
来解析并执行。所以我把它移到一个新建的函数体内。 然后保留旧的那行代码,以供参考。
我注意到的另一个点,变量 p
指向了存在于 HTML 的 DOM 结构里 id 为 p
的标签,就是那个之前我包装过的 pre 标签。事实上,元素标签可以通过他们的 id 用 JavaScript 来获取,只要 id 仅由字母数字组成。这里,我通过 document.getElementById("p")
来让它更加直观。
var delay = 64;
var p = document.getElementById("p"); // < --------------
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
for (n += 7, i = delay, P = 'p.\n'; i -= 1 / delay; P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) {
j = delay / i; p.innerHTML = P;
}
};
var n = setInterval(draw, delay);
下一步,我声明了变量 i
、p
和 j
,然后把他们放在函数的顶部。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay; // < ---------------
var P ='p.\n';
var j;
for (n += 7; i > 0 ;P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) {
j = delay / i; p.innerHTML = P;
i -= 1 / delay;
}
};
var n = setInterval(draw, delay);
我把 for
循环分解成 while
循环。只保留了 for
的CHECK_EVERY_LOOP部分(for的三个部分分别是RUNS_ONCE_ON_INIT; CHECK_EVERY_LOOP; DO_EVERY_LOOP),然后分别把其他的代码移到循环的内外部。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay;
var P ='p.\n';
var j;
n += 7;
while (i > 0) { // <----------------------
//Update HTML
p.innerHTML = P;
j = delay / i;
i -= 1 / delay;
P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2];
}
};
var n = setInterval(draw, delay);
接着我将会展开 P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]
中的三元操作(判断条件 ? true时运行 :false时运行
)
i % 2
是用来检测 i
是奇数还是偶数,如果 i 是偶数,则返回 2。如果是奇数,则返回 (i % 2 * j - j + n / delay ^ j) & 1
的计算结果(更多的是这种情况)。
最终,这个返回值被当作索引,被用于获取字符串P的某个字符,因此它可以写成 P += P[index]
。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay;
var P ='p.\n';
var j;
n += 7;
while (i > 0) {
//Update HTML
p.innerHTML = P;
j = delay / i;
i -= 1 / delay;
let index;
let iIsOdd = (i % 2 != 0); // <---------------
if (iIsOdd) { // <---------------
index = (i % 2 * j - j + n / delay ^ j) & 1;
} else {
index = 2;
}
P += P[index];
}
};
var n = setInterval(draw, delay);
下一步,我会把 index = (i % 2 * j - j + n / delay ^ j) & 1
里的 & 1
分解到另外的 if 表达式里。
这是一种聪明的方法来检测括号内的值是奇数还是偶数,如果是偶数则返回 0,反之返回 1.&
是与的位运算符。与的逻辑如下:
- 1 & 1 = 1
- 0 & 1 = 0
因此 something & 1
则可以看成把“something”转化成二进制,接着在 1 的前面填充对应数量的 0,从而保持和 something 的长度一致,然后仅仅返回与运算的最后一位。例如,5的二进制是 101
。如果我们和 1
进行与运算,将会得到如下结果:
101
AND 001
001
或者说,5是一个奇数,5 & 1
的结果是 1。用 JavaScript 的控制台很容易可以证明下面这个逻辑。
0 & 1 // 0 - even return 0
1 & 1 // 1 - odd return 1
2 & 1 // 0 - even return 0
3 & 1 // 1 - odd return 1
4 & 1 // 0 - even return 0
5 & 1 // 1 - odd return 1
注意,我将上述 index
的剩余部分重命名为 magic
。因此这些代码加上展开 & 1
后的代码看起来是下面这样的。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay;
var P ='p.\n';
var j;
n += 7;
while (i > 0) {
//Update HTML
p.innerHTML = P;
j = delay / i;
i -= 1 / delay;
let index;
let iIsOdd = (i % 2 != 0);
if (iIsOdd) {
let magic = (i % 2 * j - j + n / delay ^ j);
let magicIsOdd = (magic % 2 != 0); // &1 < --------------------------
if (magicIsOdd) { // &1 <--------------------------
index = 1;
} else {
index = 0;
}
} else {
index = 2;
}
P += P[index];
}
};
var n = setInterval(draw, delay);
接下来,我将会分解 P += P[index]
到一个 switch 表达式里。现在我们可以很清晰的知道 index的值只可能为 0、1 和 2 中的一个。也可以知道 P 的初始化总是 var P ='p.\n'
, index 为 0 时指向 p
,为 1 时指向 .
,为 2 时指向 \n
—— 新的一行字符串。
var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
var i = delay;
var P ='p.\n';
var j;
n += 7;
while (i > 0) {
//Update HTML
p.innerHTML = P;
j = delay / i;
i -= 1 / delay;
let index;
let iIsOdd = (i % 2 != 0);
if (iIsOdd) {
let magic = (i % 2 * j - j + n / delay ^ j);
let magicIsOdd = (magic % 2 != 0); // &1
if (magicIsOdd) { // &1
index = 1;
} else {
index = 0;
}
} else {
index = 2;
}
switch (index) { // P += P[index]; <-----------------------
case 0:
P += "p"; // aka P[0]
break;
case 1:
P += "."; // aka P[1]
break;
case 2:
P += "\n"; // aka P[2]
}
}
};
var n = setInterval(draw, delay);
我将简化 var n = setInterval(draw, delay)
。setInterval
会返回一个从 1 开始的整数,并且每次执行完 setInterval
之后返回值都会递增。这个整数可以在 clearInterval
方法里面用到(用来取消定时器)。在我们的代码里, setInterval
仅仅只会执行一次,所以 n 可以简单的设置为 1.
我还把 delay
重命名为 DELAY
让它看起来是一个常量。
最后但并非不重要的一点,我用括号把 i % 2 * j - j + n / DELAY
包起来,指明 ^
异或运算的执行优先度低于 %
,*
,-
,+
和/
操作。或者说,所有的运算操作都会比 ^
先执行。包装后的代码应该是这样的 ((i % 2 * j - j + n / DELAY) ^ j)
。
// 之前我把 `p.innerHTML = P;` 放错地方了,更新后,把它移出了while循环
const DELAY = 64; // approximately 15 frames per second 15 frames per second * 64 seconds = 960 frames
var n = 1;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
/**
* Draws a picture
* 128 chars by 32 chars = total 4096 chars
*/
var draw = function() {
var i = DELAY; // 64
var P ='p.\n'; // First line, reference for chars to use
var j;
n += 7;
while (i > 0) {
j = DELAY / i;
i -= 1 / DELAY;
let index;
let iIsOdd = (i % 2 != 0);
if (iIsOdd) {
let magic = ((i % 2 * j - j + n / DELAY) ^ j); // < ------------------
let magicIsOdd = (magic % 2 != 0); // &1
if (magicIsOdd) { // &1
index = 1;
} else {
index = 0;
}
} else {
index = 2;
}
switch (index) { // P += P[index];
case 0:
P += "p"; // aka P[0]
break;
case 1:
P += "."; // aka P[1]
break;
case 2:
P += "\n"; // aka P[2]
}
}
//Update HTML
p.innerHTML = P;
};
setInterval(draw, 64);
你可以在这里看到最后的结果。
第二步:理解代码
这部分将会介绍什么内容呢?不要心急,让我们一步一步来解析。
i
通过 var i = DELAY
,被初始化为 64,然后每次循环递减 1/64,等于0.015625(i -= 1 / DELAY)。循环持续到 i
小于 0 时(while (i > 0) {
)。每次执行循环,i
将会减少 1/64,所以每执行 64 次循环,i
就会减 1 (64 / 64 = 1),总得来说, i
需要执行 64 x 64 = 4096 次,之后小于 0.
之前的图片中,一共有 32 行,每行包含了 128 个字符。恰巧的是 64 x 64 = 32 x 128 = 4096。我们触发 32 次 i
为严谨的偶数的情况,i
是绝对的偶数时,i
才为偶数(非奇数 let iIsOdd = (i % 2 != 0)
; 译者提示:偶数是整数,所以2.2是奇数),例如 i
为 64,62,60等。在这 32 次里,index 通过 index = 2
赋值为 2,意味着字符串将添加 P += "\n"; // aka P[2]
从而换行,开始一行新的字符串。剩余的 127 个字符则都是 p
和 .
。
那么我们根据什么来判断何时用 p
或者 .
?
当然,之前我们就已经知道了,当 let magic = ((i % 2 * j - j + n / DELAY) ^ j)
中的 magic 是奇数的时候用 .
,如果是偶数则用 p
。
var P ='p.\n';
...
if (magicIsOdd) { // &1
index = 1; // second char in P - .
} else {
index = 0; // first char in P - p
}
但我们很难知道 magic 是奇数还是偶数,这是一个很有分量的问题。在此之前,让我们证实一些事情。
如果我们把 + n/DELAY
从 let magic = ((i % 2 * j - j + n / DELAY) ^ j)
当中移除掉,我们最终将会看到一个静态的布局,如下图
现在,让我们来看看移除了 + n/DELAY
的 magic
。如何能得到上面漂亮的图片。
(i % 2 * j - j) ^ j
注意到每次循环里,我们都会执行:
j = DELAY / i;
i -= 1 / DELAY;
换句话说,我们可以将上述表达式中的 j
用 i
表示,变成 j = DELAY/ (i + 1/DELAY)
,但因为 1/DELAY 是一个非常小的数值,所以我们暂时去掉 + 1/DELAY
并简化成 j = DELAY/i = 64/i
// 译者注
为何这里不是 j = DELAY/ (i - 1/DELAY)呢?
原因:
i -= 1 / DELAY
转化成i = i - 1 / DELAY
这里有 2 个
i
可以代入消元,但是因为j
的表达式在i
前面,所以j
取得i
应
该是自减前的i
,故i = i + 1/ DELAY
因此我们可以重写 (i % 2 * j - j) ^ j
为 (i % 2 * 64/i - 64/i) ^ 64/i
让我们用在线的图形计算器来绘制那些函数
首先,我们来绘制下 i%2
的图
从下面的图形可以看出,y 的值区间在 0 到 2 之间。
如果我们绘制 64 / i
则会得到如下图形
如果我们绘制 (i % 2 * 64/i - 64/i)
表达式,我们将得到一个混合了上面两张图的一个图形,如下
最后,如果我们把2个函数同时绘制出来,将会是如下的图(红线为 j
的关系图)
我们能从图形里知道些什么?
让我们回忆下我们要去解答的问题:如何得到如下静止图像:
好的,我们知道如果 (i % 2 * j - j) ^ j
的值是一个偶数,那么我们将添加 p
,如果是一个奇数则添加 .
。
让我们专注在图形的前面 16 行,i
的值在 64 到 32 之间。
异或运算在 JavaScript 里会把小数点右边的值忽略掉,所以它看起来和执行 Math.floor
的效果一样。
其实当2个对比位都是 1 或者 0 的时候, 异或操作会返回0。
这里我们的 j
初始值为 1,且慢慢的递增趋向于 2,但始终小于 2,所以我们可以把它当成 1 来处理(Math.floor(1.9999) === 1
),为了得到结果为 0 (意味着是偶数),我们还需要异或表达式的左边也是 1,使得返回一个 p
给我们。
换句话说,每条藏青色的倾斜线都相当于我们图像中的一行,因为前面16行的 j
值总是介于 1 和 2 之间,而唯一能得到奇数值的方法是让 (i % 2 * j - j) ^ j
(也可以说i % 2 * i/64 - i/64
或者藏青色的倾斜线)大于 1 或小于 -1。
为了将这个地方讲清楚,这里有一些Javascript控制台的输出,0 或者 -2 意味着结果是偶数,1 则是奇数。
1 ^ 1 // 0 - even p
1.1 ^ 1.1 // 0 - even p
0.9 ^ 1 // 1 - odd .
0 ^ 1 // 1 - odd .
-1 ^ 1 // -2 - even p
-1.1 ^ 1.1 // -2 - even p
如果我们观察下我们的图形,可以看出原点右边的斜线大部分都是大于 1 或者小于 -1(几乎没有偶数,或者说几乎没有 p),且越靠后(靠近原点)越如此。第 16 行几乎介于 2 和 -2 之间。第 16 行之后,我们可以看到图形是另外一种模式。
16 行之后 j
超过了 2,使得结果发生了变化。现在当藏青色的斜线大于 2 ,小于 -2 ,或者在1和-1之间且不等于的时候,我们将会得到一个偶数。这也是为什么在 17 行之后我们会在一行内看到两组和两组以上的 p
。
如果你仔细看动图的最底部几行,你会发现这几行不符合上面的规则,图表曲线看起来起伏非常大。
现在让我们把 + n/DELAY
加回来。在代码里我们可以看到 n
的初始值是 8 (初始是 1 ,但是每次定时器被调用时就加 7),它会在每次执行定时器时增加 7。
当 n
变成 64,图形会变成如下样子。
可以注意到,j
总是 ~1(这里的 ~ 是近似的意思),但是现在红斜线的左半边位于 62-63 区间的值无限趋近于 0,红斜线的右半边位于 63-64 则无限趋近与 1。因为我们的字符按64到62的顺序排列,那么我们可以猜测斜线的 63-64 部分(1^1=0 是偶数)添加的是一段 p
,左边 62-63 部分(1^0=1 是奇数)添加的是一段 .
。就像普通的英语单词一样,从左到右的添加上。
用 HTML 渲染出来的话,将会看到下图(你可以自己在 codepen 改变 n
来观看效果)。这和我们的预期一致。
这一时刻 p
的数量已经增长了一定的数量。例如第一行里面就有一半的值是偶数,从现在起,一大段的p
和 s
将移动他们的位置。
为了说明这一点,我们可以看到当 n
在下一个定时器里增加了 7 时,图形就会有稍微的变化
注意,第一行的斜线(在 64 附近)已经稍微移动了 1 小格,假设 4 个方格代表 128 个字符,1 个方格 相当于 32 个字符,那么 1 个小格则相当于 32/5=6.4 个字符(大约)。正如下图所示,我们可以看到第一行实际上向右移动了 7 个字符。
最后一个例子。就是当定时器被调用超过 7 次时(n 等于 64+9x7)会发生什么。
对于第一行,j
还等于 1。现在红斜线的上部分在 64 左右的值趋向于 2,下部分趋向于 1。这个图片将会翻转,因为现在 1^2 = 3 是奇数-输出.
而 1^1 = 0 是偶数- 输出p
。所以我们预期在一大段 p
之后会是一大段 .
。
他会这么渲染。
自此,图形将会以这种形式无限循环下去。
我希望我解释清楚了。我不认为自己有能力写出这样的代码,但是我很享受理解它的过程。
iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。
翻译 | 一行 JavaScript 代码的逆向工程的更多相关文章
- 一行JavaScript代码获取页面中的所有超链接地址
因为我喜欢收集Web开发类的网址,平时对网址就很敏感. 我总结了一下我收集网址的几个阶段: 1.纯手工阶段,傻傻的阶段. 在这个阶段,主要是收集一些在页面中展现出来的网址,就是说,如果网址出现在HTM ...
- FineUI(专业版)实现百变通知框(无JavaScript代码)!
博客园已经越来越不公正了,居然说我这篇文章没有实质的内容!! 我其实真的想问哪些通篇几十个字,没任何代码和技术分享,嚷嚷着送书的文章的就能雄霸博客园首页几天,我这篇文章偏偏就为管理员所容不下. 其实我 ...
- 一行js代码识别Selenium+Webdriver及其应对方案
有不少朋友在开发爬虫的过程中喜欢使用Selenium + Chromedriver,以为这样就能做到不被网站的反爬虫机制发现. 先不说淘宝这种基于用户行为的反爬虫策略,仅仅是一个普通的小网站,使用一行 ...
- [转] 翻译-高质量JavaScript代码书写基本要点 ---张鑫旭
by zhangxinxu from http://www.zhangxinxu.com本文地址:http://www.zhangxinxu.com/wordpress/?p=1173 原文作者:St ...
- 编写高质量JavaScript代码绳之以法(The Essentials of Writing High Quality JavaScript)翻译
原文:The Essentials of Writing High Quality JavaScript 才华横溢的Stoyan Stefanov,在他写的由O'Reilly初版的新书<Java ...
- 一行能装逼的JavaScript代码
一行能装逼的JavaScript代码 2016-06-28 野狗 一行神奇的JS代码,当时我就震 惊了,这不就是传说中的ZB神奇么… … 哈哈. 写本篇文章的缘由是之前看到了一段js代码,如下: (! ...
- 一行能装逼的JavaScript代码的延伸
前段就是坑,入坑水真深. 先看看一个黑科技, 纳尼,这是什么东西. (!(~+[])+{})[--[~+""][+[]]*[~+[]] + ~~!+[]]+({}+[])[[~!+ ...
- [label][翻译][JavaScript-Translation]七个步骤让你写出更好的JavaScript代码
7 steps to better JavaScript 原文链接: http://www.creativebloq.com/netmag/7-steps-better-javascript-5141 ...
- 【探索】机器指令翻译成 JavaScript
前言 前些时候研究脚本混淆时,打算先学一些「程序流程」相关的概念.为了不因太枯燥而放弃,决定想一个有趣的案例,可以边探索边学. 于是想了一个话题:尝试将机器指令 1:1 翻译 成 JavaScript ...
随机推荐
- Nodejs进阶:使用DiffieHellman密钥交换算法
## 简介 Diffie-Hellman(简称DH)是密钥交换算法之一,它的作用是保证通信双方在非安全的信道中安全地交换密钥.目前DH最重要的应用场景之一,就是在HTTPS的握手阶段,客户端.服务端利 ...
- MSF初体验 - kali linux 入侵XP系统
最近做某安全竞赛平台的比赛,真正开始接触Metasploit这一渗透神器,同时也是装逼神器(2333-.),下面记录一下初步使用Metasploit的过程.首先在百度百科摘录了一段关于Metasplo ...
- JavaSE二次学习之标识符和编程命名相关的内容
前段时间阿里开源了<阿里巴巴 JAVA 开发手册>,里面详细叙述了有关编程命名.sql规约.工程规约等内容,作为一个初学者,只讨论一下-编程规约-的部分. 这几天又重新回去看了看JavaS ...
- python专题-爬虫功能
在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材. 我们最常规的做法就是通过鼠标右键,选择另存为.但有些图片鼠标右键的 ...
- 优先队列的二叉堆Java实现
package practice; import edu.princeton.cs.algs4.StdRandom; public class TestMain { public static voi ...
- 我的项目经验总结——CDN镜像:1(初探)
前言 其实,这个标题有些大,作为一个小白,只是在实际工作中经常听闻我司的CDN服务如何如何牛B……而且我司的云服务还拿到了工信部的CDN牌照……那么,作为一个研发仔,怎么能不去了解和熟悉呢?!不过,这 ...
- SQL Server锁类型
SQL Server锁类型(SQL)收藏 1. HOLDLOCK: 在该表上保持共享锁,直到整个事务结束,而不是在语句执行完立即释放所添加的锁. 2. NOLOCK:不添加共享锁和排它锁,当这个选项生 ...
- C#设计模式(5)-建造者模式
引言 上一篇介绍了设计模式中的抽象工厂模式-C#设计模式(3)-抽象工厂模式,本篇将介绍建造者模式: 点击这里查看全部设计模式系列文章导航 建造者模式简介 建造者模式是将一个复杂对象的构建与表示分离, ...
- Swift4.0 Array详解
数组的介绍 数组(Array)是一串有序的由相同类型元素构成的集合,数组中的集合元素是有序的,可以重复出现.在Swift中数组类型是Array,是一个泛型集合.数组分成:可变数组和不可变数组,分别使用 ...
- 使用设置sa用户登录sql server2008
今天在net项目中添加数据库过程中出现了小问题,就是使用sql server身份验证没登录成功,经过一番调试,终于解决问题. 使用sa账户登录sql server 2008 的方法步骤如下: 1.首先 ...