setTimeout函數之循環和閉包

setTimeout函數之循環和閉包

前言

之前對于setTimeout的一個經典問題的理解總是感到很迷惑,現在好像清晰一點了,所以把我的理解寫下來,我對js的理解也不深入,如果有錯誤,請務必指出。以免誤導其他看到這篇文章的小白。-.

先來點開胃菜

先看看這種很常見的問題吧:

for (var i = 1; i <= 5; i++) {
    setTimeout( function timer(){
        console.log(i);
    },i*1000);
}

上面這個例子來自《你不知道的JavaScript》,相信這種類似的問題也很常見,我最早見到這個例子是在TypeScript的文檔里面,當時就不是很理解,對于輸出的結果也就是強行記憶為“console.log(i)執行的時候i變為6了”,但對于這中間的大致流程卻是十分模糊,以至于我當時錯誤的以為for循環和同步異步有什么關系。

正篇

先說下上面代碼的運行結果:運行時會以每秒一次的頻率輸出五次6.
先拋開為什么結果是五次6這個問題,為什么這個頻率會是每秒一次呢?可能大家剛開始的時候會有這種想法:“setTimeout函數的作用不是推遲執行里面的回調函數嗎?那結果就應該是for循環第一次時延遲一秒輸出1,然后是for循環第二次,延遲兩秒輸出2然后以此類推或者到最后i的值為6所以應該是以6秒為周期循環打印6?
這里就遇到了第一個坑,對setTimeout函數理解有偏差。

為什么是每秒一次呢?

SF來幫忙

這是我在segmentfault上看到的一個問題。原問題鏈接。請參考第二個回答。

setTimeout的延遲不是絕對精確的;
setTimeout的意思是傳遞一個函數,延遲一段時候把該函數添加到隊列當中,并不是立即執行;
所以說如果當前正在運行的代碼沒有運行完,即使延遲的時間已經過完,該函數會等待到函數隊列中前面所有的函數運行完畢之后才會運行;也就是說所有傳遞給setTimeout的回調方法都會在整個環境下的所有代碼運行完畢之后執行;

觀察下面的代碼:

setTimeout(function(){
        console.log("here");
    }, 0);
    var i = 0;
    //具體數值根據你的計算機CPU來決定,達到延遲效果就好
    while (i < 3000000000) {
        i ++;
    }
    console.log("test");

試著將上面的代碼運行了遍下,結果為在過了一段時間之后,先打印了test,然后才是here。而且需要注意的是,上面的代碼寫的是setTimeout(..,0),如果按照之前錯誤地將setTimeout函數理解為延遲一段時間執行,那這里把時間賦為0豈不是馬上執行了?而實驗結論則印證了上面"setTimeout的意思是傳遞一個函數,延遲一段時間把該函數添加到隊列中,并不是立即執行“的結論。(涉及到線程,異步,事件循環的知識我現在理解得還不到位,所以暫且不表)

現在再來想想為什么是每秒一次

再回到最初的那個問題,剛進入for循環的時候,i為1,所以相對于現在延遲一秒將timer函數添加到隊列當中,然后for循環還要繼續啊,并沒有等一秒再繼續循環啊,然后進行第二次循環,這時候i為2,所以相對于現在延遲兩秒將timer函數送進隊列。以此類推。for循環的時間忽略不計的話,timer函數就以每秒一次的頻率執行啦。

為什么每次都顯示6呢?

這個問題我個人覺得與異步和閉包都有關系。
首先和異步的關系上文已經說了。

和閉包的關系

先要清楚,什么是閉包?過去我也把閉包和立即執行函數錯誤的混為一談,看著立即執行函數表達式的括號我就天真地以為:用括號把函數包裹起來,這不就是”閉“包嗎?

《你不知道的JavaScript》書中,對閉包的解釋大概是這樣的:對函數類型的值進行傳遞時,保留對它被聲明的位置所處的作用域的引用。
也許上面這句話我總結得比較晦澀,但原書對這個問題解釋得要清晰一些,可以看看原書47頁。

那timer函數是在setTimeout函數中被聲明的吧?在執行timer函數中的console.log(i)的時候,這個i是多少呢?在timer函數中沒有i的聲明啊。那就繼續向外層的作用域找,終于在全局作用域下找到了i為多少了。

var的疑問

再來看看那個for循環,for(var i = 1; i <= 5; i++){...},在這里其實隱含著函數作用域和塊作用域的的陷阱。在這段代碼中用var聲明的變量i的作用域在哪呢?是在當前作用域還是{}所包裹的內部呢?其實我們只要明確剛才這段代碼相當于下面的代碼就清除i的作用域在哪了。

var i;
for(i = 1; i <= 5; i++)

這就是每次的輸出都是6的原因

所以,當timer函數第一次執行的時候,在執行console.log(i)的時候,這個時候的i其實是全局作用域下的i,這個時候循環是已經結束了,這時候i為6.(再次提醒不要錯誤地認為要等timer函數執行之后才會繼續循環,再看看什么是異步);

那么問題來了

那么,怎么改動上面的代碼讓結果依次為1,2,3,4,5呢?最簡單的辦法就是將var改為let,原因是let創建了塊作用域。(具體是怎么回事暫且不表,可以用babel將ES6轉換為ES5查看結果。但是原理和下面要講的類似)
所以,再想想為什么會每次的輸出都是6呢?是因為每次執行到console.log(i)的時候這個i是全局作用域下的i啊,那怎么才能讓這個i為每次循環時的i呢?即怎么才能在每次循環時”捕獲“到i的副本呢

不要急,先來看看為什么可以用立即執行函數表達式。

所以下面的代碼有用嗎?

for (var i = 1; i <= 5; i++) {
    (function() {
        setTimeout( function timer() {
            console.log(i);
        },i*1000 );
    })();
}

上面這個例子同樣是來自《你不知道的JavaScript》。我以前錯誤地認為,立即執行函數表達式,這是立即執行啊,所以里面的timer也立即執行了,所以就能輸出1,2,3,4,5了。
先說答案,這樣當然是不行的,這里的立即執行也只是立即執行了setTimeout函數,而setTimeout函數的作用也就是將timer函數延遲一段時間添加到隊列,所以這個立即執行表達式在這里有沒有都一樣。我之前錯誤的想法也是受到了”立即執行“這四個字的誤導。先來看看一個正確答案:

for (var i = 1; i <= 5; i++) {
    (function() {
        var j = i;
        setTimeout( function timer() {
            console.log(j);
        },i*1000 ); //這一行將i*1000改為j*1000也行,并不影響
    })();
}

發現這個答案和上面的錯誤答案的區別了嗎?其實我們是用立即執行函數表達式創造了新的函數作用域將timer函數包裹了起來,并用j捕獲了每次循環時的i,這樣在運行到console.log(j)的時候顯示的就是每次循環時的i值啦。
同理還有這樣的寫法:

for (var i = 1; i <= 5; i++) {
    let j = i;
    setTimeout(function timer() {
        console.log(j);
    },j*1000);
}

還有一些其他寫法這里就不一一列舉了,原理都是和作用域相關。其實上面這個涉及到let的例子和塊作用域相關,這里就不展開了。

總結

異步決定了這段代碼打印i的頻率,閉包和作用域的知識決定了這個i是多少以及怎樣改寫這段代碼。
總覺得這篇文章還有一些欠缺,希望大家能指正。 uuu

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容

  • 特別說明,為便于查閱,文章轉自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 491評論 0 0
  • 問題 一、什么是閉包? 有什么作用? 1.什么是閉包①JavaScript高級程序設計第三版定義閉包是指有權訪問另...
    鴻鵠飛天閱讀 475評論 0 0
  • 官方中文版原文鏈接 感謝社區中各位的大力支持,譯者再次奉上一點點福利:阿里云產品券,享受所有官網優惠,并抽取幸運大...
    HetfieldJoe閱讀 5,620評論 16 88
  • 以前看過好多文檔,對于閉包不是很理解,再讀《你不知道的JavaScript》上卷之后,終于明白了,感謝這本書,把自...
    晴風無眠閱讀 436評論 0 1
  • 1 如何加入 第一步,回答五個問題 A 為什么要加入“愛吃”? B 目標收入是多少? C一天能保證幾小時空閑時間發...
    1bf7bab8493a閱讀 354評論 1 1