如何輸出1-5題詳解

如何輸出1-5

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

  • 使用IIFE產生閉包
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的即時狀態得以保存,問題迎刃而解。

  • 使用bind產生閉包
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)
  }
}
  • 救世主let
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,函數參數列表被認爲是一個實質的做用域,兩個方法的實質都是同樣的。

相關文章
相關標籤/搜索