for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) } // 6 6 6 6 6
i的做用域鏈:babel
timer scope -> global scope閉包
i變量會循着以上的做用域鏈來查找,固然此時是在global做用域鏈找到了i,可是setTimeout是異步的,會在1s、2s、3s、4s、5s時運行,這個時候都去找全局的i,此時循環早就完成了,i變爲6,因此輸出5個6,解決問題的關鍵有兩種思路app
爲每次循環的函數保存這次循環的變量狀態異步
1.利用實參函數
PS: 參數列表 在ES6時,已經被明肯定義爲一個做用域oop
在timer scope函數體內查找i變量後,在向上查找以前,會查找函數實參列表中的變量,咱們將i傳入,並在調用函數時使用這個i,每一個函數實例可以保存本身的實參狀態,就能解決i的共享問題this
for (var i = 1; i <= 5; i++) { setTimeout( function timer(j) { console.log(j) }, i * 1000, i ) } // 1 2 3 4 5
2.產生做用域來保存狀態prototype
咱們知道,前面的問題是狀態最終都指向了global上的i,做用域能夠保存變量的狀態,若是在timer和global做用域之間添加一個新的做用域,並讓這個中間做用域持久化(造成閉包),那麼也能解決當前的i共享問題code
for (var i = 1; i <= 5; i++) { ;(function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i) } // 1 2 3 4 5
分析作用域
(function(){})()
上述代碼稱之爲IIFE(當即執行表達式),能夠在不污染現有做用域狀況下建立一個新的做用域(函數內部),在循環過程當中,每一個IIEF中的setTimeout回調函數都由於引用了j變量而產生了閉包效應,j的即時狀態得以保存,問題迎刃而解。
for (var i = 1; i <= 5; i++) { setTimeout(function timer(i) { console.log(i) }.bind(null, i), i * 1000) }
跟上面相似,bind在內部產生了一個閉包,可以在循環中保存當前循環的i的狀態,bind的核心實現以下
Function.prototype.bind = function (context) { var fn = this return function () { return fn.apply(context, args) } }
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }
咱們使用前面的方法,都須要改動函數自己,而let語法能夠說是js幫咱們作了產生做用域的操做,而又不須要改動太多,僅僅一個符號改變便可享受,美滋滋
let的babel轉譯結果
var _loop = function _loop(i) { setTimeout(function timer() { console.log(i); }, i * 1000); }; for (var i = 1; i <= 5; i++) { _loop(i); }
babel在這種狀況下轉譯也是利用一個函數產生了一個獨立的做用域,且生成了閉包
此問題的核心在於利用做用域和閉包保存瞬時狀態,利用函數參數列表和產生做用域都是基於此思路,特別是在ES6,函數參數列表被認爲是一個實質的做用域,兩個方法的實質都是同樣的。