js--ES6-Promise與經典迴圈閉包題

weixin_33797791發表於2017-03-25
  • 首先一開始是下面這樣子的:
    for (var i = 0; i < 5; i++) {
    setTimeout(function() {
    console.log(new Date, i);
    }, 1000);
    }
    console.log(new Date, i);
    上面的程式碼等同於:
    for (var i = 0; i < 5; i++) {
    function xxx() {
    console.log(new Date, i);
    }
    setTimeout(xxx, 1000);
    }
  • 注意:setTimeout裡面傳入的第一個匿名函式,等價於在setTimeout語句外面定義的一個函式。所以它的閉包範圍是變數i所在的作用域,所以可以訪問到i.
  • 解析:大家都知道 i 是外面的變數,所有的setTimeout裡面的函式都會指向同一個 i ,所以最終輸出:

Sat Mar 25 2017 12:40:11 GMT+0800 (中國標準時間) 5
undefined
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5
VM87:3 Sat Mar 25 2017 12:40:12 GMT+0800 (中國標準時間) 5

  • 下面改造開始:要讓控制檯能夠輸出1234
  1. 可以用閉包:(每次把新的變數i傳進一個匿名的立即執行函式,每次 j
    都能得到不同的 i ,因為j在匿名函式的作用域內,函式的執行作用域每執行一次都會重新生成,所以每次的 j 都不是同一個)
    for (var i = 0; i < 5; i++) {
    (function(j) { // j = i
    setTimeout(function() {
    console.log(new Date, j);
    }, 1000);
    })(i);
    }

            console.log(new Date, i);
    
  2. 可以用傳參法:(其實是把閉包的匿名函式擴充套件出來)
    function wrapper(j) {
    // function fn() {
    // console.log(new Date, j);
    // }
    // setTimeout(fn,1000);
    // 上面全部的寫法,等價於:
    setTimeout(function () {
    console.log(new Date, j);
    },1000);
    }
    for (var i = 0; i < 5; i++) {
    wrapper(i);
    }
    console.log(new Date, i);
    輸出如下:

Sat Mar 25 2017 13:20:57 GMT+0800 (中國標準時間) 5
undefined
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 0
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 1
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 2
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 3
VM94:8 Sat Mar 25 2017 13:20:58 GMT+0800 (中國標準時間) 4

注意:這裡有一個容易犯錯的點,setTimeout的第一個引數要求的是一個函式的引用,而不是執行一個函式,所以只能傳入函式名xxx的形式,而不能傳入xxx(a,b),這也意味著不能在xxx上直接傳參。
  • 所以,要傳參的話,只能在setTimeout外面再包裹一層函式,然後定製編寫xxx函式。
再補充一個重要知識點,如果一定要在xxx中傳參,又不想用閉包,可以使用setTimeout的第3個引數,從第3個引數往後的引數,都會傳入xxx裡作為形參使用。
  • 例如:上面的程式碼也可以寫成:
    setTimeout(function(j) {
    console.log(new Date, j); //可以輸出01234
    }, 1000 ,i);
  • 如果硬是在setTimeout()中傳入的xxx()的形式,那麼只會以正常任務的方式立即執行xxx(),而不會放入任務佇列裡去,也就是定時器失效。
  • 例如:
    for (var i=0; i<5; i++){
    function xxx(j) {
    console.log(new Date,j)
    }
    setTimeout(xxx(i),1000);
    }
    console.log(new Date, i);
    以上程式碼的輸出如下:

Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 0
VM2209:3 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 1
VM2209:3 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 2
VM2209:3 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 3
VM2209:3 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 4
VM2209:7 Sat Mar 25 2017 13:37:27 GMT+0800 (中國標準時間) 5
undefined

  • 解析:可以看到,所有時間幾乎是同一時刻輸出的,而且是按012345的順序,最後退出函式返回undefined,也就是說在退出函式前,setTimeout裡的xxx就已經執行了,並沒有進入任務佇列。由此可以說明,setTimeout的第一個引數期待的是函式名,而不是一個函式的執行。
    • 當然,如果xxx()執行後返回的是一個函式,理論上也可以設定定時器函式,但傳參又出現問題,太過複雜,現實暫時沒有遇到必須這樣的問題,不作討論。
ES6及Promise登場
  • 上述還可以用ES6及Promise來實現,具體如下:
    var tasks=[];
    function output(j) {
    var promise = new Promise( function(resolve, reject) {
    setTimeout(function () {
    console.log(new Date(), j);
    resolve(j);
    // console.log("這是一個小補充喲"+j);
    },1000 * i);
    });
    promise.then(function (j) {
    // console.log("這是then裡的一點小補充喲"+j);
    });
    return promise;
    }
    for (var i=0;i<5;i++){
    tasks.push(output(i)); //執行順序:首先在這裡將定時器設定好,
    // 也就是迴圈設定定時器,由於執行時間很快,每次迴圈的間隔可以忽略不計,
    // 所以可以認為是設定了5個時間分別為0~4s的定時器,已經開始計時。
    // 計時後返回promise物件,放在tasks陣列中
    }
    // 最後,在這裡相當於是一個總的監聽器,當前面4個任務都resolve以後,執行最後一個設定定時器任務,
    // 到時間以後,執行輸出5
    // 關於執行順序,前4個task是靠定時器的時間差別來決定先後輸出順序的,最後一個5的task,是依靠非同步回撥來執行的。
    Promise.all(tasks).then(function () {
    setTimeout(function () {
    console.log(new Date(), i);
    },1000);
    });
  • 解析:// 關於任務佇列:
    // 首先第一趟主任務佇列走下來,執行了設定定時任務,將promise物件放入tasks陣列,並設定好then回撥的工作。
    // 然後第二趟,執行定時任務佇列,執行consolo.log語句,
    // 然後遇到resolve,需要呼叫相應的then裡面的回撥語句(如果有的話)。
    // 但是注意,這裡呼叫then的時機,是在本次任務的主程式碼執行完畢後,
    // 也就是說,如果setTimeout語句中的resolve()後面還有執行語句,要先執行那些語句,最後才執行resolve對應的then回撥。
  • 要確定resolve相應的回撥語句的執行順序,可以看下面的輸出結果(將程式碼裡的兩句console.log去掉註釋即可):

Sat Apr 01 2017 13:15:47 GMT+0800 (中國標準時間) 0
(index):41 這是一個小補充喲0
(index):45 這是then裡的一點小補充喲0
(index):39 Sat Apr 01 2017 13:15:48 GMT+0800 (中國標準時間) 1
(index):41 這是一個小補充喲1
(index):45 這是then裡的一點小補充喲1
(index):39 Sat Apr 01 2017 13:15:49 GMT+0800 (中國標準時間) 2
(index):41 這是一個小補充喲2
(index):45 這是then裡的一點小補充喲2
(index):39 Sat Apr 01 2017 13:15:50 GMT+0800 (中國標準時間) 3
(index):41 這是一個小補充喲3
(index):45 這是then裡的一點小補充喲3
(index):39 Sat Apr 01 2017 13:15:51 GMT+0800 (中國標準時間) 4
(index):41 這是一個小補充喲4
(index):45 這是then裡的一點小補充喲4
(index):60 Sat Apr 01 2017 13:15:52 GMT+0800 (中國標準時間) 5

最後強調一遍,resolve的回撥函式是在本輪“事件迴圈”結束時執行,setTimeout(fn, 0)在下一輪“事件迴圈”開始時執行。
  • 如果覺得上面的例子太複雜,看下面:
    setTimeout(function () {
    console.log('three');
    }, 0);

      Promise.resolve().then(function () {
        console.log('two');
      });
     
      console.log('one');
      // one
      // two
      // three

相關文章