先從一個小題目開始: 以下代碼的輸出結果是? 下麵還有加強版: // 2 function test2(value) { value = value || 'default 2'; console.log(value); } setTimeout(test2, 1000, 2.1); // T2-1 ...
先從一個小題目開始: 以下代碼的輸出結果是?
// 1 function test1 () { console.log(1) }; setTimeout(test1, 1000); // T1-1setTimeout(test1(), 1000); // T1-2 setTimeout(console.log(1.1), 1000); // T1-3
下麵還有加強版:
// 2 function test2(value) { value = value || 'default 2'; console.log(value); } setTimeout(test2, 1000, 2.1); // T2-1 setTimeout(test2(), 1000, 2.2); // T2-2 setTimeout(test2(2.3), 1000, 2.31); // T2-3 // 3 function test3(value) { value = value || 'default 3'; console.log(value); return test3; } setTimeout(test3, 1000, 3.1); // T3-1 setTimeout(test3(), 1000, 3.2); // T3-2 setTimeout(test3(3.3), 1000, 3.31) // T3-3 // 4 for(var i = 0; i < 5; i++) { // T4-1 console.log(i); } for(var i = 0; i < 5; i++) { // T4-2 setTimeout(function() { console.log(i); }, 1000 * i); } for(var i = 0; i < 5; i++) { // T4-3 setTimeout(function(i) { console.log(i); }, 1000 * i); } for(var i = 0; i < 5; i++) { // T4-4 (function(i) { setTimeout(function() { console.log(i); }, i * 1000); })(i); } for(var i = 0; i < 5; i++) { // T4-5 (function() { setTimeout(function() { console.log(i); }, i * 1000); })(i); } for(var i = 0; i < 5; i++) { // T4-6 setTimeout((function(i) { console.log(i); })(i), i * 1000); } // 5 function someLoop(){ var tag = true; var temp = 0; setTimeout(function(){ tag = !tag; console.log(tag) }, 1000); while(tag) { temp++; } } someLoop(); // T5-1setTimeout 示例2
如果這些對你都是小菜一碟,咳咳,你可以自己回去玩了,這裡已經沒有啥對你有價值的東西了,我們他日再敘。
如果你對這些代碼有些疑惑,那可以坐下來一起探討一下人生。
先看第一段:
// 1 function test1 () { console.log(1) }; setTimeout(test1, 1000); // T1-1 setTimeout(test1(), 1000); // T1-2 setTimeout(console.log(1.1), 1000); // T1-3
T1-1 這裡是最常見的使用方法了, 這個就不用多說了,1s 以後輸出 1;
===我是分割線===
T1-2 與 T1-1 的區別就是第一個參數不再是函數,而是函數調用。那是不是1s 以後再調用這個函數呢?在 chrome console 里看一下運行的結果
立即輸出了1,又輸出了33,1s 以後啥都沒有。
這裡有3個小問題:1是咋輸出的;33是咋輸出的;1s 以後啥都沒有又是咋回事。
先說33,註意最上邊 “// 1” 左邊有一個向右的箭頭,這代表你在 console 里輸入的代碼;33左邊有一個向左的箭頭,這個意思是代碼執行後返回的值;33上面兩個橫線中間的值沒有左箭頭也沒有右箭頭,是代碼里 console.log() 列印的結果。
這裡33代表著 setTimeout() 方法的返回值,返回的是一個計時器,可以用來以後取消定時用的,這個33就是定時器的 id。這裡對我們代碼的執行並沒有啥意義,下麵就忽略這個計時器的 id 了。
要想說清楚輸出1和1s 以後啥都沒有的問題,必須先說說 JavaScript 里的事件迴圈機制了。
setTimeout(fn, delay) 是在指定時間以後,將 fn 推入到事件迴圈的消息隊列當中去,然後 js 執行引擎跑完執行棧里的代碼以後,就立即從這個消息隊列里依次取出函數來執行。
這個將 fn 推入消息隊列的函數用 js 模擬類似這樣:
function pushToMessageQueue(fn){ console.log(fn); if(fn && Object.prototype.toString.call(fn).toLowerCase() === '[object function]') { // add fn to Message Queue // ...... } }
// 我並沒有研究過瀏覽器底層代碼實現,如有錯誤,還請留言指出,謝謝。
執行 setTimeout(fn, delay) 的時候,底層調用這個方法,將 fn 作為參數傳遞進去。
於是,當我們執行 setTimeout(test1(), 1000) 的時候,底層實際上將 test1() 作為參數傳遞給 pushToMessageQueue(fn) 函數。
這實際上發生了兩件事情:
1,將 test1() 作為參數賦值給 fn;類似於這樣:
var fn = test1();
2,運行 pushToMessageQueue 函數邏輯。
在第一步里進行參數傳遞的時候,實際上是立即執行了 test1(),並將返回值賦值給fn。所以才會立即列印 1。test1 函數並沒有返回值,預設返回 undefined,於是 pushToMessageQueue 函數的實際參數是 undefined,於是就沒有任何東西被推進執行棧里,所以1s 以後就啥都沒有執行。
至此,T1-2的疑問得到解決。瞭解了這些,再理解接下里的幾種情況就容易許多。
===我是分割線===
T1-3 與 T1-2 很像,不過一個是函數調用,一個是語句,但它們運行的過程都是類似的。先把 console.log(1.1) 賦值給 fn,於是立即列印出 1.1,fn 的值為 undefined,於是1s 以後啥都沒有執行。
第一段代碼輸出結果如下:(忽略計時器 id)
===我是分割線===
下麵看第二段:
// 2 function test2(value) { value = value || 'default 2'; console.log(value); } setTimeout(test2, 1000, 2.1); // T2-1 setTimeout(test2(), 1000, 2.2); // T2-2 setTimeout(test2(2.3), 1000, 2.31); // T2-3
T2-1 與 T1-1 的區別是多了一個參數 2.2,setTimeout(fn, delay) 常見的寫法就是兩個參數,一個是要執行的函數,一個是延遲時間,多餘的參數什麼意思呢?看一下 MDN 上的資料(中文資料在此)
var timeoutID = scope.setTimeout(function[, delay, param1, param2, ...]); var timeoutID = scope.setTimeout(function[, delay]); var timeoutID = scope.setTimeout(code[, delay]);
這裡的 scope 預設是 window。
這家伙有3種用法:
第二種用法,就是最常見的用法;
第三種用法,第一個參數是一個字元串,執行的時候解析為 js 語句再執行。這個用法已經不推薦了,完全可以用第二種代替。
第一種用法,就是在第二種之上加了更多的參數,這些參數會在第一個參數函數 fn 執行的時候作為參數傳遞,即 fn(param1, param2, param3) 這樣。
於是 T2-1 意思就是 1s 以後執行 test2(2.1),結果就是1s 以後列印 2.1。
===我是分割線===
T2-2 與 T1-2 類似,立即執行 test2(),列印 default 2,由於 test2 函數並沒有返回值,1s 以後也不會有任何函數被推入消息隊列,多餘的參數也沒有意義。
===我是分割線===
T2-3 里 test2 帶了一個參數2.3,立即列印2.3,1s 以後啥都沒有。
第二段代碼輸出結果如下:
===我是分割線===
第三段:
// 3 function test3(value) { value = value || 'default 3'; console.log(value); return test3; } setTimeout(test3, 1000, 3.1); // T3-1 setTimeout(test3(), 1000, 3.2); // T3-2 setTimeout(test3(3.3), 1000, 3.31) // T3-3
T3-1 比較好理解,1s 以後執行 test3,並傳入參數 3.1,實際執行的是 test3(3.1),所以1s 以後列印3.1;執行以後返回的 test3 沒有被任何變數接收。
===我是分割線===
T3-2 立即執行 test3(),立即列印 default 3,並將返回值 test3 推入消息隊列,1s 以後執行 test3(3.2),列印 3.2;
===我是分割線===
T3-3 立即執行 test3(3.3),立即列印 3.3,並將返回值 test3 推入消息隊列,1s 以後執行 test3(3.31),列印 3.31;
第三段代碼輸出結果如下:
===我是分割線===
第四段:
// 4 for(var i = 0; i < 5; i++) { // T4-1 console.log(i); } for(var i = 0; i < 5; i++) { // T4-2 setTimeout(function() { console.log(i); }, 1000 * i); } for(var i = 0; i < 5; i++) { // T4-3 setTimeout(function(i) { console.log(i); }, 1000 * i); } for(var i = 0; i < 5; i++) { // T4-4 (function(i) { setTimeout(function() { console.log(i); }, i * 1000); })(i); } for(var i = 0; i < 5; i++) { // T4-5 (function() { setTimeout(function() { console.log(i); }, i * 1000); })(i); } for(var i = 0; i < 5; i++) { // T4-6 setTimeout((function(i) { console.log(i); })(i), i * 1000); }
T4-1:
輸出 0 1 2 3 4
===我是分界線===
T4-2:
前面說到,setTimeout(fn, delay) 是在 delay 後將 fn 推入消息隊列,等主進程執行完執行棧中的代碼以後再執行執行消息隊列里的回調。這裡 for 迴圈就是執行棧中的代碼,setTimeout 第一個參數里的回調都在指定時機被推入消息隊列,迴圈完畢以後再執行。而此時的 i 已經變成了5,因此,列印結果是,立即列印一個5,然後接下來每秒再列印一個5。一共5個5。
5 (1s 後)5(2s 後)5 (3s 後)5 (4s 後)5
===我是分界線===
T4-3:
和上一個的區別是,回調里多了一個 i 參數。但在執行的時候並沒有實參傳遞給這個回調,因此 i 就是 undefined,輸出結果跟上一個類似,只是把 5 換成了 undefined。
undefined (1s 後)undefined(2s 後)undefined (3s 後)undefined (4s 後)undefined
===我是分界線===
T4-4:
迴圈體內部變成了一個立即執行函數,並把 i 作為參數傳遞。於是就利用閉包保存了對每一個 i 的引用。
0 (1s 後)1(2s 後)2 (3s 後)3 (4s 後)4
===我是分界線===
T4-5
與上一個的區別是,迴圈體內的立即執行函數沒有形參,於是傳遞的參數 i 就沒有參數來接收,於是匿名函數內部的 i 引用的其實都是外面迴圈里的 i。本質上和 T4-2並沒有區別,輸出也是一樣的。
5 (1s 後)5(2s 後)5 (3s 後)5 (4s 後)5
===我是分界線===
T4-6
setTimeout 的第一個參數變成了一個立即執行函數,這和 T1-2 是類似的,只不過外部多了一個迴圈而已。
0 1 2 3 4 (1s 後)無輸出
第四段 輸出如下
===我是分界線===
第五段
// 5 function someLoop(){ var tag = true; var temp = 0; setTimeout(function(){ tag = !tag; console.log(tag) }, 1000); while(tag) { temp++; } } someLoop(); // T5-1
根據上面的分析,這個應該很好理解。這段代碼是個死迴圈,永遠不會輸出。
結果:
參考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop