摘要:下一步,我聲明了變量和,然后把他們放在函數的頂部。注意,我將上述的剩余部分重命名為。會返回一個從開始的整數,并且每次執行完之后返回值都會遞增。行之后超過了,使得結果發生了變化。這和我們的預期一致。這一時刻的數量已經增長了一定的數量。
原文地址:Reverse Engineering One Line of JavaScript
原文作者:Alex Kras
譯者:李波
校對者:冬青、小蘿卜
幾個月前,我看到一個郵件問:有沒有人可以解析這一行 JavaScript 代碼
這一行代碼會被渲染成下圖的效果。你可以在這里用瀏覽器打開來觀看。這是 Mathieu ‘p01’ Henri 寫的,你還可以在作者的網站www.p01.org里看到更多很酷的例子。
好的!我決定接受挑戰
第一步:讓代碼變得可讀第一件事,讓 HTML 文件里只有 HTML 代碼,然后把 JavaScript 代碼放到 code.js 文件里。我還用 id="p" 來包裝 pre 標簽。
index.html
我注意到變量 k 只是一個常量,所以把它移出來,然后重命名為 delay。
code.js
var delay = 64; var draw = "for(n+=7,i=delay,P="p. ";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. ";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. "; 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. ";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. "; 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. ";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. "; 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. ";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. "; 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. ";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. "; 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. ", index 為 0 時指向 p,為 1 時指向 .,為 2 時指向 —— 新的一行字符串。
var delay = 64; var p = document.getElementById("p"); // var draw = "for(n+=7,i=delay,P="p. ";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. "; 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 += " "; // 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. ";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. "; // 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 += " "; // 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 += " "; // aka P[2] 從而換行,開始一行新的字符串。剩余的 127 個字符則都是 p 和 .。
那么我們根據什么來判斷何時用 p 或者 . ?
當然,之前我們就已經知道了,當 let magic = ((i % 2 * j - j + n / DELAY) ^ j) 中的 magic 是奇數的時候用 . ,如果是偶數則用 p。
var P ="p. "; ... 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前端高效開發實戰》已在亞馬遜、京東、當當開售。
滬江Web前端上海團隊招聘【Web前端架構師】,有意者簡歷至:zhouyao@hujiang.com
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88441.html
摘要:前端日報精選一行代碼的逆向工程譯只需四個步驟使用實現頁面過渡動畫如何實現一個基于的模板引擎解剖組件的多種寫法與演進深入理解筆記擴展對象的功能性中文基礎系列一之實現抽獎刮刮卡橡皮擦掘金小游戲個人文章和最常用的特征眾成翻譯常用語法總 2017-08-08 前端日報 精選 一行 JavaScript 代碼的逆向工程【譯】只需四個步驟:使用 React 實現頁面過渡動畫如何實現一個基于 DOM...
摘要:花點時間搞清楚中的分號規則吧不管你喜歡結尾帶分號或省略分號的模式分號允許的場景分號一般允許出現在大部分語句的末尾,比如等栗子僅有一個分號可以表示空語句在中合法,比如可解析為三個空語句空語句可用于輔助產生語法合法的解析結果,如如果沒有末尾的 花點時間搞清楚JS中的分號規則吧~~~不管你喜歡結尾帶分號或省略分號的模式 分號允許的場景 分號一般允許出現在大部分語句(statement)的末尾...
摘要:跟非常相似,但是在開始嘗試你第一個之前,也需要了解兩者之間的一些差異。推薦的方式是使用提供的。能用到組件中的或者上以啟用這個組件的觸摸事件。 華翔,Web前端開發工程師著作權歸作者所有,轉載請聯系作者獲得授權。 showImg(https://segmentfault.com/img/bVUliz?w=640&h=235); React-Native已經誕生有兩年左右了,自從適配了An...
摘要:前端日報精選從零學習技術棧新版本及簡介石墨表格之應用實戰一道面試題目引發的思考新為帶來的性能變化中文第期前端部署采坑記個最基本的面試問題及答案上插件開發簡介一開發入門掘金插件開發簡介二如何添加瀏覽器擴展白名單掘金層疊相關知識指北掘 2017-09-09 前端日報 精選 從零學習React技術棧:React 新版本及 ES6 簡介石墨表格之 Web Worker 應用實戰一道面試題目引發...
閱讀 3077·2023-04-26 00:53
閱讀 3522·2021-11-19 09:58
閱讀 1693·2021-09-29 09:35
閱讀 3279·2021-09-28 09:46
閱讀 3852·2021-09-22 15:38
閱讀 2692·2019-08-30 15:55
閱讀 3006·2019-08-23 14:10
閱讀 3822·2019-08-22 18:17