setTimeout()的參數:
大家都知道setInterval()和setTimeout()可以接收兩個參數,第一個參數是需要回調的函數,必須傳入的參數,第二個參數是時間間隔,毫秒數,可以省略。但其實他可以接收更多的參數,那么這些參數是干什么用的呢?從第三個參數開始,依次用來表示傳入回調函數的參數。
setTimeout(function(a,b){ console.log(0+a+b);//這里打印的是:7 },1000,3,4);
注意:IE 9.0及以下版本,只允許setTimeout有兩個參數,不支持更多的參數
如果想向回調函數傳參,可以用bind()。
setTimeout( function(a,b){}.bind(3,4), 1000 );
clearTimeout
setTimeout函數,返回一個表示計數器編號的整數值,將該整數傳入clearTimeout函數,就可以取消對應的定時器。 clearTimout()有以下語法: clearTimeout(timeoutID) 要使用 clearTimeout( ), 我們設定 setTimeout( ) 時, 要給予這 setTimout( ) 一個名稱, 這名稱就是 timeoutID , 我們叫停時, 就是用這 timeoutID來叫停, 這是一個自定義名稱。
var id1 = setTimeout(f,1000); //id1就是timeoutID var id2 = setInterval(f,1000); //id2就是timeoutID clearTimeout(id1); clearInterval(id2);
setTimeout()的this指向:
對于JAVAscript中的this指向問題,之前也是困擾了我好久,哎呀,哪兒有那么難嘛,其實一句話就是說:誰調用的就是指向誰啊!意思就是說調用的對象是誰this就是指向誰。
var x = 1; var obj = { x: 2, y: function(){ console.log(this.x); } }; setTimeout(obj.y,1000); // 1
why?不是說了哪個對象調用的就是指向哪個對象的嘛,這里不是setTimeout函數調用了obj對象里面的y方法嗎,那不還是被setTimeout調用了嗎,對啊,沒錯啊,就是setTimeout調用的,但是setTimeout函數是屬于window的,知道吧,所以setTimeout的對象是window,所以一切都明了了。
var x = 1; var obj = { x: 2, y: function(){ console.log(this.x); } }; setTimeout(obj.y.bind(obj),1000); // 2 function Animal(login) { this.login = login; this.sayHi = function() { console.log(this.login); //undefined } } var dog = new Animal('John'); setTimeout(dog.sayHi, 1000);
等到dog.sayHi執行時,它是在全局對象中執行,但是this.login取不到值。
setTimeout()之延遲時間為0
console.log('a'); setTimeout(function(){ console.log('b'); },0); console.log('c'); console.log('d'); // a // c // d // b
我也不截圖了。 知道為什么嗎,理論上他延遲時間為0不是應該馬上執行嗎,不是的。因為setTimeout運行機制說過,必須要等到當前腳本的同步任務和“任務隊列”中已有的事件,全部處理完以后,才會執行setTimeout指定的任務。 也就是說,setTimeout的真正作用是,在“任務隊列”的現有事件的后面再添加一個事件,規定在指定時間執行某段代碼。setTimeout添加的事件,會在下一次Event Loop執行。好吧,對事件循環不清楚的推薦看看JavaScript 運行機制詳解:再談Event Loop
事件循環中的setTimeout()
眾所周知,Javascript引擎(以下簡稱JS引擎)是單線程的,在某一個特定的時間內只能執行一個任務,并阻塞其他任務的執行,也就是說這些任務是串行的。這樣的話,用戶不得不等待一個耗時的操作完成之后才能進行后面的操作,這顯然是不能容忍的,但是實際開發中我們卻可以使用異步代碼來解決。
當異步方法比如這里的setTimeout(),或者ajax請求、DOM事件執行的時候,會交由瀏覽器內核的其他模塊去管理。當異步的方法滿足觸發條件后,該模塊就會將方法推入到一個任務隊列中,當主線程代碼執行完畢處于空閑狀態的時候,就會去檢查任務隊列,將隊列中第一個任務入棧執行,完畢后繼續檢查任務隊列,如此循環。前提條件是主線程處于空閑狀態,這就是事件循環的模型。
setTimeout(function () { console.log("b"); },0) console.log("a"); // a // b
原理,就是上面兩段話當中解釋的,執行時把setTimeout()放入任務隊列中去,主線程執行完主線程的任務之后去任務隊列里面執行setTimeout出來執行。
setTimeout(function(){ console.log(1111); },0) while (true) {};
這里控制臺是永遠不會輸出東西的,因為主線程已經造成了死循環,主線程一直是不會空閑的,他不會到任務隊列里面去執行拿setTimeout函數來執行。
首先我們還是來看那道大家再熟悉不過的前端面試題:
for (var i = 1;i <= 5;i ++) { setTimeout(function timer() { console.log(i) },i * 1000) }
我想剛入門的童鞋或者對JS作用域、閉包以及事件循環等概念不了解的童鞋會想當然的認為這道題的答案應該是:
第一次循環,隔一秒輸出1;
第二次循環,隔兩秒輸出2;
第三次循環,隔三秒輸出3;
第四次循環,隔四秒輸出4;
第五次循環,隔五秒輸出5;
或者還有同學預期的結果是分別輸出數字1~5,每秒一次,每次一個。
但實際結果大家去控制臺打印了都知道:以一秒的頻率連續輸出五個6!
相信對于很多童鞋第一次看到這個結果是懵的,包括我第一次看到結果是懵逼的!
然而還沒等你反應過來,面試官又要求你改動一下代碼,要它以一秒的頻率分別輸出1,2,3,4,5。如果你不了解或者沒有深入理解JS中的作用域、閉包以及事件循環,那么就可以和面試官說拜拜了。
這道題涉及到的知識點我上面已經提到過兩次,這里我們還是先簡單地過一下這些知識點:
1、作用域:這里我引用《你不知道的javascript》中的一個比喻,可以把作用域鏈想象成一座高樓,第一層代表當前執行作用域,樓的頂層代表全局作用域。我們在查找變量時會先在當前樓層進行查找,如果沒有找到,就會坐電梯前往上一層樓,如果還是沒有找到就繼續向上找,以此類推。到達頂層后(全局作用域),可能找到了你所需的變量,也可能沒找到,但無論如何查找過程都將停止。
2、閉包:我的理解是在傳遞函數類型的變量時,該函數會保留定義它的所在函數的作用域。讀起來可能比較繞,或者可以簡單的這么理解,A函數中定義了B函數并且它返回了B函數,那么不管B函數在哪里被調用或如何被調用,它都會保留A函數的作用域。
3、事件循環:這個概念深入起來很復雜,下面新開一個段落只說一些跟本文相關的內容。
說起事件循環,不得不提起任務隊列。事件循環只有一個,但任務隊列可能有多個,任務隊列可分為宏任務(macro-task)和微任務(micro-task)。XHR回調、事件回調(鼠標鍵盤事件)、setImmediate、setTimeout、setInterval、indexedDB數據庫操作等I/O以及UI rendering都屬于宏任務(也有文章說UI render不屬于宏任務,目前還沒有定論),process.nextTick、Promise.then、Object.observer(已經被廢棄)、MutationObserver(html5新特性)屬于微任務。注意進入到任務隊列的是具體的執行任務的函數。比如上述例子setTimeout()中的timer函數。另外不同類型的任務會分別進入到他們所屬類型的任務隊列,比如所有setTimeout()的回調都會進入到setTimeout任務隊列,所有then()回調都會進入到then隊列。當前的整體代碼我們可以認為是宏任務。事件循環從當前整體代碼開始第一次事件循環,然后再執行隊列中所有的微任務,當微任務執行完畢之后,事件循環再找到其中一個宏任務隊列并執行其中的所有任務,然后再找到一個微任務隊列并執行里面的所有任務,就這樣一直循環下去。這就是我所理解的事件循環。來,還是看個栗子:
console.log('global') setTimeout(function () { console.log('timeout1') new Promise(function (resolve) { console.log('timeout1_promise') resolve() }).then(function () { console.log('timeout1_then') }) },2000) for (var i = 1;i <= 5;i ++) { setTimeout(function() { console.log(i) },i*1000) console.log(i) } new Promise(function (resolve) { console.log('promise1') resolve() }).then(function () { console.log('then1') }) setTimeout(function () { console.log('timeout2') new Promise(function (resolve) { console.log('timeout2_promise') resolve() }).then(function () { console.log('timeout2_then') }) }, 1000) new Promise(function (resolve) { console.log('promise2') resolve() }).then(function () { console.log('then2') })
我們來一步一步分析以上代碼:
1)、首先執行整體代碼,“global”會被第一個打印出來。這是第一個輸出.
2)、執行到第一個setTimeout時,發現它是宏任務,此時會新建一個setTimeout類型的宏任務隊列并派發當前這個setTimeout的回調函數到剛建好的這個宏任務隊列中去,并且輪到它執行時要延遲2秒后再執行
3)、代碼繼續執行走到for循環,發現是循環5次setTimeout(),那就把這5個setTimeout中的回調函數依次派發到上面新建的setTimeout類型的宏任務隊列中去,注意,這5個setTimeout的延遲分別是1到5秒。此時這個setTimeout類型的宏任務隊列中應該有6個任務了。再執行for循環里的console.log(i),很簡單,直接輸出1,2,3,4,5,這是第二個輸出。
4)、再執行到new Promise,Promise構造函數中的第一個參數在new的時候會直接執行,因此不會進入任何隊列,所以第三個輸出是"promise1",上面有說到Promise.then是微任務,那么這里會生成一個Promise.then類型的微任務隊列,這里的then回調會被push進這個隊列中。
5)、再繼續走,執行到第二個setTimeout,發現是宏任務,派發它的回調到上面setTimeout類型的宏任務隊列中去。
6)、再走到最后一個new Promise,很明顯,這里會有第四個輸出:"promise2",然后它的then中的回調也會被派發到上面的Promise.then類型的微任務隊列中去。
7)、第一輪事件循環的宏任務執行完成(整體代碼可以看做宏任務)。此時微任務隊列中只有一個Promise.then類型微任務隊列,它里面有兩個任務。宏任務隊列中也只有一個setTimeout類型的宏任務隊列。
8)、下面執行第一輪事件循環的微任務,很明顯,會分別打印出"then1",和"then2"。分別是第五和第六個輸出。此時第一輪事件循環完成。
9)、開始第二輪事件循環:執行setTimeout類型隊列(宏任務隊列)中的所有任務。發現都有延時,但延時最短的是for循環中第一次循環push進來的那個setTimeout和上面第5個步驟中的第二個setTimeout,它們都只延時1s。它們會被同時執行,但前者先被push進來,所以先執行它!它的作用就是打印變量i,在當前作用域找變量i,木有!去它上層作用域(這里是全局作用域)找,找到了,但此時的i早已是6了。(為啥不是5,那你得去補補for循環的執行流程了~)所以這里第七個輸出是延時1s后打印出6。
10)、緊接著執行第二個setTimeout,它會先后打印出"timeout2"和"timeout2_promise",這分別是第八和第九個輸出。但這里發現了then,又把它push到上面已經被執行完的then隊列中去。
11)、這里要注意,因為出現了微任務then隊列,所以這里會執行該隊列中的所有任務(此時只有一個任務),即打印出"timeout2_then"。這是第十個輸出
11)、繼續回過頭來執行宏任務隊列,此時是執行延時為2s的第一個setTimeout和for循環中第二次循環的那個setTimeout,跟上面一樣,前者是第一個被push進來的,所以它先執行。這里會延時1秒(原因下面會解釋)分別輸出“timeout1”和“timeout1_promise”,但發現了里面也有一個then,于是push到then微任務隊列并立即執行,輸出了"timeout1_then"。緊接著執行for中第二次循環的setTimeout,輸出6。注意這三個幾乎是同時被打印出來的。他們分別是第十一到十三個輸出。
12)、再就很簡單了,把省下的for循環中后面三次循環被push進來的setTimeout依次執行,于是每隔1s輸出一個6,連續輸出3次。
13)、第二輪事件循環結束,全部代碼執行完畢。
global 1 2 3 4 5 promise1 promise2 then1 then2 //延遲1s 6 timeout2 timeout2_promise timeout2_then //延遲1s timeout1 17 timeout1_promise 20 timeout1_then 6 //每隔1s輸出3個6
這里解釋下為什么上面第11步不是延遲2秒再輸出“timeout1”和“timeout1_promise”,這時需要理解setTimeout()延時參數的意思,這個延遲時間始終是相對主程序執行完畢的那個時間算的 ,并且多個setTimeout執行的先后順序也是由這個延遲時間決定的。
再回過頭來看上面那個問題,理解了事件循環的機制,問題就很簡單了。for循環時setTimeout()不是立即執行的,它們的回調被push到了宏任務隊列當中,而在執行任務隊列里的回調函數時,變量i早已變成了6。那如何得到想要的結果呢?很簡單,原理就是需要給循環中的setTimeout()創建一個閉包作用域,讓它執行的時候找到的變量i是正確的。
知道了原理,解決方案就很多了,下面給出5種方案,
1)引入IIFE
for(var i = 0;i<5;i ++) { (function(i){ setTimeout(function timer() { console.log(i) }, i * 1000); })(i); }
2)利用ES 6引入的let關鍵字
for(let i = 0;i<5;i++) { setTimeout(function timer(){ console.log(i); }, i * 1000); }
for 循環頭部的let 聲明還會有一個特殊的行為。這個行為指出變量在循環過程中不止被聲明一次,每次迭代都會聲明。隨后的每個迭代都會使用上一個迭代結束時的值來初始化這個變量。
3)利用ES 5引入的bind函數
for (var i=1; i<=5; i++) { setTimeout( function timer(i) { console.log(i); }.bind(null,i), i*1000 ); }
4)利用setTimeout第三個參數
for (var i=1; i<=5; i++) { setTimeout( function timer(i) { console.log(i); }, i*1000,i ); }
注:setTimeout函數第三個參數及以后的參數都可以作為timer函數的參數。
5)把setTimeout用一個方法單獨出來形成閉包
var loop = function (i) { setTimeout(function timer() { console.log(i); }, i*1000); }; for (var i = 1;i <= 5; i++) { loop(i); }