翻譯 | 一行 JavaScript 代碼的逆向工程

幾個月前,我看到一個郵件問:有沒有人能夠解析這一行 JavaScript 代碼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 文件裏只有 HTML 代碼,而後把 JavaScript 代碼放到 code.js 文件裏。我還用 id="p" 來包裝 pre 標籤。java

index.html瀏覽器

<script src="code.js"></script>
<pre id="p"></pre>

我注意到變量 k 只是一個常量,因此把它移出來,而後重命名爲 delay架構

code.jsapp

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);

下一步,我聲明瞭變量 ipj,而後把他們放在函數的頂部。網站

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/DELAYlet magic = ((i % 2 * j - j + n / DELAY) ^ j) 當中移除掉,咱們最終將會看到一個靜態的佈局,以下圖

如今,讓咱們來看看移除了 + n/DELAYmagic。如何能獲得上面漂亮的圖片。

(i % 2 * j - j) ^ j

注意到每次循環裏,咱們都會執行:

j = DELAY / i;
i -= 1 / DELAY;

換句話說,咱們能夠將上述表達式中的 ji 表示,變成 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 的數量已經增加了必定的數量。例如第一行裏面就有一半的值是偶數,從如今起,一大段的ps 將移動他們的位置。

爲了說明這一點,咱們能夠看到當 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前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

滬江Web前端上海團隊招聘【Web前端架構師】,有意者簡歷至:zhouyao@hujiang.com

相關文章
相關標籤/搜索