幾個月前,我看到一個郵件問:有沒有人能夠解析這一行 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);
下一步,我聲明瞭變量 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.&
是與的位運算符。與的邏輯以下:
所以 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前端高效開發實戰》已在亞馬遜、京東、噹噹開售。