Alex Kras氏がブログで投稿した記事「Reverse Engineering One Line of JavaScript (Posted Jul 13, 2017)」を翻訳してご紹介しています。
なお、この記事は原著者の許諾を得て翻訳・掲載しています。
数か月前、この1行のJavaScriptの親要素を削除することができるかどうかを尋ねる電子メールが来ました。
1 | <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) |
2 |
この行は、下の画像のようにレンダリングされます。あなたのブラウザでも見ることができますよ。これは、Mathieu ‘p01’ Henri(www.p01.orgの著者)が作成したもので、www.p01.orgでは他にも多くのかっこいいデモを見ることができます。
その挑戦、受けて立ちましょう!
パート1. コードを読みやすくする
まずは大事なことから始めましょう。僕はHTMLをHTML形式で保存し、JavaScriptをcode.jsファイルに移動させました。また僕は、引用符でpをid="p"にwrap(特定の要素を囲むこと)しました。
index.html
1 | <script src="code.js"></script> |
2 | <pre id="p"> |
3 |
僕は変数kが単なる定数であることに気づいたので、それを行から移動させ、delayに名前を変更しました。
code.js
1 | var delay = 64; |
2 | 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"; |
3 | var n = setInterval(draw, delay); |
4 |
次に、var drawは単なる文字列でした。setIntervalは評価される関数または文字列を受け入れることができるので、var drawはsetInterval内でevalとして実行されていました。僕はそれを実際の関数に移動させました。しかし、参考のために古い行も置いています。
僕が気づいたもう1つのことは、要素pが、HTMLで宣言されたid pのDOM要素を実際に参照したことです。id pは、先ほど引用符で囲んだものです。ID名が英数字のみで構成されている限り、要素はID名を使ってJavaScriptから参照できることがわかりました。より直観的にするためにdocument.getElementById("p")を追加しました。
1 | var delay = 64; |
2 | var p = document.getElementById("p"); // < -------------- |
3 | // 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"; |
4 | var draw = function() { |
5 | for (n += 7, i = delay, P = 'p.\n'; i -= 1 / delay; P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) { |
6 | j = delay / i; p.innerHTML = P; |
7 | } |
8 | }; |
9 | var n = setInterval(draw, delay); |
10 |
次に、変数i、p、jを宣言し、それらを関数の先頭に移動させました。
1 | var delay = 64; |
2 | var p = document.getElementById("p"); |
3 | // 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"; |
4 | var draw = function() { |
5 | var i = delay; // < --------------- |
6 | var P ='p.\n'; |
7 | var j; |
8 | for (n += 7; i > 0 ;P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) { |
9 | j = delay / i; p.innerHTML = P; |
10 | i -= 1 / delay; |
11 | } |
12 | }; |
13 | var n = setInterval(draw, delay); |
14 |
僕は、forループをwhileループとして外に追い出しました。forの3つの部分(RUNS_ONCE_ON_INIT、CHECK_EVERY_LOOP、DO_EVERY_LOOP)のCHECK_EVERY_LOOP部分のみを保持し、他のすべてをループ本体の内部または外部に移動させました。
1 | var delay = 64; |
2 | var p = document.getElementById("p"); |
3 | // 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"; |
4 | var draw = function() { |
5 | var i = delay; |
6 | var P ='p.\n'; |
7 | var j; |
8 | n += 7; |
9 | while (i > 0) { // <---------------------- |
10 | //Update HTML |
11 | p.innerHTML = P; |
12 | |
13 | j = delay / i; |
14 | i -= 1 / delay; |
15 | P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]; |
16 | } |
17 | }; |
18 | var n = setInterval(draw, delay); |
19 |
P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2];の三項演算子(condition ? do if true : do if false)を展開しました。
i%2は、iが偶数か奇数かをチェックしています。もしiが偶数だったら、それは2を返します。もしiが奇数だったら、(i % 2 * j - j + n / delay ^ j) & 1;のマジックナンバーを返します。(これについては後ほど少し説明します)。
最後に、そのインデックスを文字列Pにオフセットするために使用したので、P += P[index];になりました。
1 | var delay = 64; |
2 | var p = document.getElementById("p"); |
3 | // 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"; |
4 | var draw = function() { |
5 | var i = delay; |
6 | var P ='p.\n'; |
7 | var j; |
8 | n += 7; |
9 | while (i > 0) { |
10 | //Update HTML |
11 | p.innerHTML = P; |
12 | |
13 | j = delay / i; |
14 | i -= 1 / delay; |
15 | |
16 | let index; |
17 | let iIsOdd = (i % 2 != 0); // <--------------- |
18 | |
19 | if (iIsOdd) { // <--------------- |
20 | index = (i % 2 * j - j + n / delay ^ j) & 1; |
21 | } else { |
22 | index = 2; |
23 | } |
24 | |
25 | P += P[index]; |
26 | } |
27 | }; |
28 | var n = setInterval(draw, delay); |
29 |
僕はindex = (i % 2 * j - j + n / delay ^ j) & 1の& 1;を別のif文として外に追い出しました。
これは、かっこ内の結果が奇数か偶数かをチェックし、偶数の場合は0、奇数の場合は1を返す賢い方法です。&はビット単位のAND演算子です。ANDのロジックは以下のとおりです:
- 1 & 1 = 1
- 0 & 1 = 0
したがって、something & 1は「something(何か)」を2進表現に変換します。また、何かの長さに合わせて、必要に応じて1の先頭にいくつかの0を埋め込み、最後のビットのANDだけを返します。例えば、5を二進法で表すと101であり、ANDと1を付けると、以下のようになります:
1 | 101 |
2 | AND 001 |
3 | 001 |
4 |
言い換えれば、5は奇数であり、5 & 1の結果は1です。JavaScriptコンソールでこのロジックが有効であることを確認するのは簡単です。
1 | 0 & 1 // 0 - even return 0 |
2 | 1 & 1 // 1 - odd return 1 |
3 | 2 & 1 // 0 - even return 0 |
4 | 3 & 1 // 1 - odd return 1 |
5 | 4 & 1 // 0 - even return 0 |
6 | 5 & 1 // 1 - odd return 1 |
7 |
僕は、残りのindexをmagicという名前に変更したので、展開された&1のコードは以下のようになります。
1 | var delay = 64; |
2 | var p = document.getElementById("p"); |
3 | // 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"; |
4 | var draw = function() { |
5 | var i = delay; |
6 | var P ='p.\n'; |
7 | var j; |
8 | n += 7; |
9 | while (i > 0) { |
10 | //Update HTML |
11 | p.innerHTML = P; |
12 | |
13 | j = delay / i; |
14 | i -= 1 / delay; |
15 | |
16 | let index; |
17 | let iIsOdd = (i % 2 != 0); |
18 | |
19 | if (iIsOdd) { |
20 | let magic = (i % 2 * j - j + n / delay ^ j); |
21 | let magicIsOdd = (magic % 2 != 0); // &1 < -------------------------- |
22 | if (magicIsOdd) { // &1 <-------------------------- |
23 | index = 1; |
24 | } else { |
25 | index = 0; |
26 | } |
27 | } else { |
28 | index = 2; |
29 | } |
30 | |
31 | P += P[index]; |
32 | } |
33 | }; |
34 | var n = setInterval(draw, delay); |
35 |
次に、僕はP += P[index];をswitch文に展開しました。今ではもう、インデックスは3つの値(0、1、または2)のうち1つでなければならないことは明らかです。Pが常にvar P ='p.\n';の値で初期化されることも明らかです。ここで、0はpを指し、1は.を指し、2は\nという新しい行の文字を指すことにします。
1 | var delay = 64; |
2 | var p = document.getElementById("p"); |
3 | // 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"; |
4 | var draw = function() { |
5 | var i = delay; |
6 | var P ='p.\n'; |
7 | var j; |
8 | n += 7; |
9 | while (i > 0) { |
10 | //Update HTML |
11 | p.innerHTML = P; |
12 | |
13 | j = delay / i; |
14 | i -= 1 / delay; |
15 | |
16 | let index; |
17 | let iIsOdd = (i % 2 != 0); |
18 | |
19 | if (iIsOdd) { |
20 | let magic = (i % 2 * j - j + n / delay ^ j); |
21 | let magicIsOdd = (magic % 2 != 0); // &1 |
22 | if (magicIsOdd) { // &1 |
23 | index = 1; |
24 | } else { |
25 | index = 0; |
26 | } |
27 | } else { |
28 | index = 2; |
29 | } |
30 | |
31 | switch (index) { // P += P[index]; <----------------------- |
32 | case 0: |
33 | P += "p"; // aka P[0] |
34 | break; |
35 | case 1: |
36 | P += "."; // aka P[1] |
37 | break; |
38 | case 2: |
39 | P += "\n"; // aka P[2] |
40 | } |
41 | } |
42 | }; |
43 | |
44 | var n = setInterval(draw, delay); |
45 |
僕は、var n = setInterval(draw, delay); magicをクリーンアップしました。set intervalは1から始まる整数を返し、setIntervalが呼び出されるたびに1ずつ値を増加させます。この整数は、clearInterval(キャンセル)に使用できます。僕たちのケースでは、setIntervalは1回だけ呼び出され、nは単純に1に設定されました。
また僕は、単なる定数であることを忘れないように、delayをDELAYという名前に変更しました。
最後に述べるものの決して軽んずべきでないことですが、僕は、^ビット単位のXOR(排他的論理和)が%、 *、 -、 +、および/演算子より低い優先順位を持つことを指摘するために、i % 2 * j - j + n / DELAY ^ jをかっこに入れました。つまり、^が評価される前に、すべての計算が最初に実行されます。結果として(i % 2 * j - j + n / DELAY) ^ j)になります。
更新:p.innerHTML = P; //Update HTMLを誤ってwhile loop内に配置しているというご指摘を受けましたので、移動させました。
1 | const DELAY = 64; // approximately 15 frames per second 15 frames per second * 64 seconds = 960 frames |
2 | var n = 1; |
3 | var p = document.getElementById("p"); |
4 | // 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"; |
5 | |
6 | /** |
7 | * Draws a picture |
8 | * 128 chars by 32 chars = total 4096 chars |
9 | */ |
10 | var draw = function() { |
11 | var i = DELAY; // 64 |
12 | var P ='p.\n'; // First line, reference for chars to use |
13 | var j; |
14 | |
15 | n += 7; |
16 | |
17 | while (i > 0) { |
18 | |
19 | j = DELAY / i; |
10 | i -= 1 / DELAY; |
11 | |
12 | let index; |
13 | let iIsOdd = (i % 2 != 0); |
14 | |
15 | if (iIsOdd) { |
16 | let magic = ((i % 2 * j - j + n / DELAY) ^ j); // < ------------------ |
17 | let magicIsOdd = (magic % 2 != 0); // &1 |
18 | if (magicIsOdd) { // &1 |
19 | index = 1; |
20 | } else { |
21 | index = 0; |
22 | } |
23 | } else { |
24 | index = 2; |
25 | } |
26 | |
27 | switch (index) { // P += P[index]; |
28 | case 0: |
29 | P += "p"; // aka P[0] |
30 | break; |
31 | case 1: |
32 | P += "."; // aka P[1] |
33 | break; |
34 | case 2: |
35 | P += "\n"; // aka P[2] |
36 | } |
37 | } |
38 | //Update HTML |
39 | p.innerHTML = P; |
40 | }; |
41 | |
42 | setInterval(draw, 64); |
43 |
ここで最終的な結果が動作しているところを見ることができます。
パート2. コードが何を実行しているか理解する
ではここで何が起こっているのでしょう?詳しく見ていきましょう。
iの初期値はvar i = DELAY;を介して64に設定され、i -= 1 / DELAY;を介してすべてのループで1/64(0.015625)ずつ値が減少していきます。while (i > 0) {のループは、iが0より大きくならなくなるまで続きます。ループが実行されるたびに、iは1/64ずつ値が減少するので、iが1減少するには64回のループが必要です(64/64 = 1)。合計では、iは0より小さくなるまで64×64 = 4096回値を減少させなければならないでしょう。
画像は32行で構成され、各行に128文字が含まれています。便利なことに、64×64 = 32×128 = 4096です。iは、厳密に偶数である場合のみ、偶数になります(奇数let iIsOdd = (i % 2 != 0);ではありません)。僕たちはそれが32回発生するようにします(iが64、62、60などであったとき)。これらの32回では、インデックスは2つの index = 2;に設定され、新しい行の文字が行P += "\n"; // aka P[2]に追加されます。各行あたりの残りの127文字は、pまたは.に設定されます。
しかし、いつpを設定し、いつ.を設定すればよいのでしょうか?
さて、僕たちは、magic let magic = ((i % 2 * j - j + n / DELAY) ^ j);が奇数のときは.に設定し、magicが偶数のときはpに設定することを知っています。
1 | var P ='p.\n'; |
2 | |
3 | ... |
4 | |
5 | if (magicIsOdd) { // &1 |
6 | index = 1; // second char in P - . |
7 | } else { |
8 | index = 0; // first char in P - p |
9 | } |
10 |
しかし、magicが奇数であるときと偶数であるときはいつでしょうか?それは百万ドルに値する素晴らしい質問です。そのことについて話す前に、もう一つ構築してみましょう。
let magic = ((i % 2 * j - j + n / DELAY) ^ j);から+ n/DELAYを削除すると、以下の静的レイアウトが完成します。全く動かないですね。
ここでは、+ n/DELAYを削除したmagicを見てみましょう。上記の可愛い絵をどうやって完成させましょうか?
(i % 2 * j - j) ^ j
ループごとに以下があることに注意してください。
1 | j = DELAY / i; |
2 | i -= 1 / DELAY; |
3 |
言い換えれば、j = DELAY/ (i + 1/DELAY)のように最終的なiについてjを表現することができますが、1/DELAYはとても小さな数字なので、図示するために+ 1/DELAYを落としてj = DELAY/i = 64/iに簡略化することができます。
(i % 2 * j - j) ^ jを(i % 2 * 64/i - 64/i) ^ 64/iとして書き換えることができると考えてみましょう。
これらの関数の一部をプロットするためにオンラインのグラフ計算機を使ってみましょう。
最初に、i%2をプロットしましょう。
これは、yの値が0から2までの範囲の素晴らしいグラフになります。
64/iをプロットすると、グラフはこのようになります。
左側の関数をプロットすると、2つを組み合わせたような種類のグラフが表示されます。
最後に、2つの関数を並べてプロットすると、以下のようになります。
これらのグラフが教えてくれること
最初の質問が何であったか思い出してみましょう。「どうしてこのような静的な画像になってしまったのか?」という質問でしたね。
magic (i % 2 * j - j) ^ jの結果が偶数の場合pを追加しなければならず、奇数の場合.を追加しなければならないことはわかりますね。
iの値が64から32までの範囲で、グラフの最初の16行に注目してみましょう。
JavaScriptのビット単位のXORは、小数点以下のすべての値を落とすため、それは数字のMath.floorを取るようなものです。
両方のビットが1の場合、または両方のビットが0の場合、0を返します。
jは1から始まり、ゆっくりと2の方向に進み、そのすぐ下にとどまるので、1として扱うことができます(Math.floor(1.9999) === 1)。そして、結果を0(偶数)にするために、左側に別の1が必要になり、pが与えられます。
つまり、斜めの緑色の線は、グラフ内の1行を表しています。最初の16行ではjは常に1より大きく2よりも小さいので、奇数の値を得るための唯一の方法は、(i % 2 * j - j) ^ jの左側、つまりi % 2 * i/64 - i/64や斜めの緑色の線が1より大きいか-1より小さいときです。
ここに、要点を十分に納得させる、JavaScriptコンソールからの出力があります。0または-2は結果が偶数であることを意味し、1は結果が奇数を意味します。
1 | 1 ^ 1 // 0 - even p |
2 | 1.1 ^ 1.1 // 0 - even p |
3 | 0.9 ^ 1 // 1 - odd . |
4 | 0 ^ 1 // 1 - odd . |
5 | -1 ^ 1 // -2 - even p |
6 | -1.1 ^ 1.1 // -2 - even p |
7 |
グラフを見ると、一番右の斜めの線が1よりも少し大きくなるか、-1よりも少し小さくなることがわかります(わずかな偶数-わずかなp)。次の行はもう少し先に進み、後の行はさらに増えていきます。16行目は、2より小さく-2より大きい範囲に留まります。16行目以降は、静的グラフがパターンをシフトさせたことがわかります。
16行目以降は、jが2の行を越え、予想される結果が反転します。緑色の斜めの線が2より大きく、-2より小さい場合、または1から-1の間(1と-1は含みません)の場合、偶数になります。17行目以降にpの2つ以上のグループが表示されているのはそのためです。
動いている画像の下の数行をよく見ると、グラフが幅広く変動しているため、それらがもはや同じパターンではないことに気付くでしょう。
さて、+ n/DELAYに話を戻しましょう。コードでは、nが8で始まることがわかります(setIntervalから1、呼び出されるすべての間隔で7)。その後、設定されたインターバルが発生するたびに7ずつ値が増加していきます。
nが64になるとグラフは以下のように変化します。
jは依然として~1(「~」は「約」という意味です)であることに注意してください。しかし、62から63の間の赤い斜めの線の左半分は~0で、63から64の間の右半分は~1です。文字は64から62に降順に追加されるので、斜めの線の63から64付近(1 ^ 1 = 0 // even)にpの束を追加し、斜めの線の62から63付近の左側 (1^0 = 1 // odd) に.の束を追加することが期待されます。それは普通の英語の単語として左から右に流れていくでしょう。
その条件のためにレンダリングされたHTMLは以下のようになります(codepenのnの値を自分でハードコーディングして自分自身で確認できます)。それは、実際に僕たちの期待にマッチしています。
この時点でpの数はその定数値まで増加しました。例えば、すべての値の最初の行の半分は常に偶数になります。pと.はその位置を変えるだけです。
図示するには、次のsetIntervalでnが7ずつ増加すると、グラフはわずかに変化します。
最初の行(64マーク付近)の斜めの線が、おおよそ1つの小さな正方形分上方に移動していることに注意してください。4つの大きな正方形が128文字を表すと仮定すると、1つの大きな正方形は32文字を表し、1つの小さな正方形は32/5 = 6.4文字(近似値)を表します。レンダリングされたHTMLを見ると、最初の行が実際に7文字分右に移動したことがわかります。
最後に1つ例を出します。ここでは、setIntervalが7回以上呼び出され、nが64+9×7と等しい場合の動作を示します。
第1行目については、jは依然として1に等しいです。ここで、64付近の赤色の斜めの線の上半分は~2であり、下半分は~1です。これは、1^2 = 3 // odd - .および1 ^ 1 = 0 //even - pであるため、画像を反転させます。そのため、ドットの束の後にpが続くことを期待することができます。
それはこのようにレンダリングします。
グラフは同様の方法で無限にループし続けます。
この説明で理にかなっていることを願っています。僕は今まで自分でこのようなものを思いついたことはありませんでしたが、それを理解しようとする試みは楽しかったです。
P.S.「HBO」ドラマシリーズのSilicon Valleyの共同プロデューサーでありライターでもあるDan Lyonsの「Disrupted: My Misadventure in the Start-Up Bubble」をチェックしてみてください。
P.P.S. Twitter、Facebook