由一道題引伸出的事件循環、let\var用法、iife、塊級做用域

沒有錯,這道題就是:javascript

for (var i = 0; i< 10; i++){
    setTimeout(() => {
        console.log(i);
    }, 1000)
} // 10 10 10 10 ...

爲何這裏會出現10次10,而不是咱們預期的0-9呢?咱們要如何修改達到預期效果呢?java

運行時&&事件循環

首先咱們得理解setTimeout中函數的執行時機,這裏就要講到一個運行時的概念。es6

函數調用造成了一個棧幀。數組

function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42

當調用 bar 時,建立了第一個幀 ,幀中包含了 bar 的參數和局部變量。當 bar 調用 foo時,第二個幀就被建立,並被壓到第一個幀之上,幀中包含了 foo 的參數和局部變量。當 foo返回時,最上層的幀就被彈出棧(剩下 bar 函數的調用幀 )。當 bar 返回的時候,棧就空了。promise

對象被分配在一個堆中,即用以表示一大塊非結構化的內存區域。併發

隊列

一個 JavaScript 運行時包含了一個待處理的消息隊列。每個消息都關聯着一個用以處理這個消息的函數。異步

事件循環(Event Loop)期間的某個時刻,運行時從最早進入隊列的消息開始處理隊列中的消息。爲此,這個消息會被移出隊列,並做爲輸入參數調用與之關聯的函數。正如前面所提到的,調用一個函數老是會爲其創造一個新的棧幀。async

函數的處理會一直進行到執行棧再次爲空爲止;而後事件循環將會處理隊列中的下一個消息(若是還有的話)。函數

與這題的關聯

這裏setTimeout會等到當前隊列執行完了以後再執行,即for循環結束後執行,而這個時候i的值已是10了,因此會打印出來10個10這樣的結果。oop

要是想獲得預期效果,簡單的刪除setTimeout也是可行的。固然也能夠這樣改setTimeout(console.log, 1000, i);i做爲參數傳入函數。

引伸出其餘

仔細查閱規範可知,異步任務可分爲 taskmicrotask 兩類,不一樣的API註冊的異步任務會依次進入自身對應的隊列中,而後等待 Event Loop 將它們依次壓入執行棧中執行。

(macro)task主要包含:script(總體代碼)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 環境)

microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 環境)

附上一幅圖更清楚的瞭解一下

Event Loop

每一次Event Loop觸發時:

  1. 執行完主執行線程中的任務。
  2. 取出micro-task中任務執行直到清空。
  3. 取出macro-task中一個任務執行。
  4. 取出micro-task中任務執行直到清空。
  5. 重複3和4。

其實promise的then和catch纔是microtask,自己的內部代碼不是。

ps: 再額外附上一道題

new Promise(resolve => {
    resolve(1);
    Promise.resolve().then(() => console.log(2));
    console.log(4)
}).then(t => console.log(t));
console.log(3);

這道題比較基礎,答案爲4321。先執行同步任務,打印出43,而後分析微任務,2先入任務隊列先執行,再打印出1。

這裏還有幾種變種,結果相似。

let promise1 = new Promise(resolve => {
  resolve(2);
});
new Promise(resolve => {
    resolve(1);
    Promise.resolve(2).then(v => console.log(v));
      //Promise.resolve(Promise.resolve(2)).then(v => console.log(v));
      //Promise.resolve(promise1).then(v => console.log(v));
      //new Promise(resolve=>{resolve(2)}).then(v => console.log(v));
    console.log(4)
}).then(t => console.log(t));
console.log(3);

不過要值得注意的是一下兩種狀況:

let thenable = {
  then: function(resolve, reject) {
    resolve(2);
  }
};
new Promise(resolve => {
  resolve(1);
  new Promise(resolve => {
    resolve(promise1);
  }).then(v => {
    console.log(v);
  });
  // Promise.resolve(thenable).then(v => {
  //   console.log(v);
  // });
  console.log(4);
}).then(t => console.log(t));
console.log(3);
let promise1 = new Promise(resolve => {
  resolve(thenable);
});
new Promise(resolve => {
  resolve(1);
  Promise.resolve(promise1).then(v => {
    console.log(v);
  });
  // new Promise(resolve => {
  //   resolve(promise1);
  // }).then(v => {
  //   console.log(v);
  // });
  console.log(4);
}).then(t => console.log(t));
console.log(3);

結果爲4312。有人可能會說阮老師這篇文章裏提過

Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))

那爲何這兩個的結果不同呢?

請注意這裏resolve的前提條件是參數是一個原始值,或者是一個不具備then方法的對象,而其餘狀況是怎樣的呢,stackoverflow上這個問題分析的比較透徹,我這裏簡單的總結一下。

這裏的RESOLVE('xxx')是new Promise(resolve=>resolve('xxx'))簡寫

  • Promise.resolve('nonThenable')RESOLVE('nonThenable')相似;
  • Promise.resolve(thenable)RESOLVE(thenable)相似;
  • Promise.resolve(promise)要根據promise對象的resolve來區分,不爲thenable的話狀況和Promise.resolve('nonThenable')類似;
  • RESOLVE(thenable)RESOLVE(promise) 能夠理解爲 new Promise((resolve, reject) => { Promise.resolve().then(() => { thenable.then(resolve) }) })

也就是說能夠理解爲Promise.resolve(thenable)會在這一次的Event Loop中當即執行thenable對象的then方法,而後將外部的then調入下一次循環中執行。

再形象一點理解,能夠理解爲RESOLVE(thenable).thenPROMISE.then.then的語法相似。

再來一道略微複雜一點的題加深印象

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

題目來源

答案

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

let、var和const用法和區別

總結一下阮老師的介紹。

  • ES6 新增了let命令,用來聲明變量。它的用法相似於var,可是所聲明的變量,只在let命令所在的代碼塊內有效。而var全局有效。
  • var命令會發生「變量提高」現象,即變量能夠在聲明以前使用,值爲undefinedlet命令改變了語法行爲,它所聲明的變量必定要在聲明後使用,不然報錯。
  • 在代碼塊內,使用let命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」(temporal dead zone,簡稱 TDZ)。
  • let不容許在相同做用域內,重複聲明同一個變量。
  • const聲明的變量不得改變值,這意味着,const一旦聲明變量,就必須當即初始化,不能留到之後賦值。const其餘用法和let相同。

與這題關聯

上面代碼中,變量ivar命令聲明的,在全局範圍內都有效,因此全局只有一個變量i。每一次循環,變量i的值都會發生改變,而循環內被賦給數組a的函數內部的console.log(i),裏面的i指向的就是全局的i。也就是說,全部數組a的成員裏面的i,指向的都是同一個i,致使運行時輸出的是最後一輪的i的值,也就是 10。

要是想獲得預期效果,能夠簡單的把var換成let

iife和塊級做用域

let實際上爲 JavaScript 新增了塊級做用域。

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

上面的函數有兩個代碼塊,都聲明瞭變量n,運行後輸出 5。這表示外層代碼塊不受內層代碼塊的影響。若是兩次都使用var定義變量n,最後輸出的值纔是 10。

塊級做用域的出現,實際上使得得到普遍應用的當即執行函數表達式(IIFE)再也不必要了。

// IIFE 寫法
(function () {
  var tmp = ...;
  ...
}());

// 塊級做用域寫法
{
  let tmp = ...;
  ...
}

與這題的關聯

若是不用let,咱們可使用iife將setTimeout包裹,從而達到預期效果。

for (var i = 0; i < 10; i++) {
  (i =>
    setTimeout(() => {
      console.log(i);
    }, 1000))(i);
}

其餘騷氣方法

for (var i = 0; i < 10; i++) {
  try {
    throw i;
  } catch (i) {
    setTimeout(() => {
      console.log(i);
    }, 1000);
  }
}

參考

阮老師es6

What's the difference between resolve(thenable) and resolve('non-thenable-object')?

Daily-Interview-Question

併發模型與事件循環

相關文章
相關標籤/搜索