深挖【let, for與定時器】引發的疑惑

来源:https://www.cnblogs.com/mxyulin/archive/2022/05/24/16305976.html
-Advertisement-
Play Games

建議您在閱覽此文之前學完W3school - JS Tutorial章節所有內容 經典的問題 在一些文章中或者工作面試問題上,會遇見這種看似簡單的經典問題。 for(var i = 0; i < 5; i++) { setTimeout(function () { console.log(i); } ...


建議您在閱覽此文之前學完W3school - JS Tutorial章節所有內容

經典的問題

在一些文章中或者工作面試問題上,會遇見這種看似簡單的經典問題。

for(var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  });
}
console.log('hello word');
/*output
'hello word'
5
5
5
5
5
*/

新手第一次看到這個問題由於沒有深入瞭解setTimeout方法的執行機制就會得到錯誤的結果。

/*output
0
1
2
3
4
'hello world'
*/

對於老鳥來說這種問題不足掛齒,但是如果你是新手正在學習js的路上如火如荼或是剛好遇到了此類問題一知半解,那麼這篇文章將帶給你視野和解答。 小小問題背後實則包含豐富有趣的學問。

認識單線程、任務隊列和事件迴圈

單線程

JS是典型的單線程語言,所謂單線程就是只能同時執行一個任務。
之所以是單線程而不是多線程,是為了避免多線程對同一DOM對象操作的衝突。比如a線程創造一div元素而b線程同時想要刪除這個div元素那麼就會出現矛盾。所以單線程是JS的核心特征。

知識延申:操作系統的進程和線程:

對於操作系統來說,一個任務就是一個進程(Process),比如打開一個瀏覽器就是啟動一個瀏覽器進程,打開一個記事本就啟動了一個記事本進程,打開兩個記事本就啟動了兩個記事本進程,打開一個Word就啟動了一個Word進程。

有些進程還不止同時乾一件事,比如Word,它可以同時進行打字、拼寫檢查、列印等事情。在一個進程內部,要同時乾多件事,就需要同時運行多個“子任務”,我們把進程內的這些“子任務”稱為線程(Thread)。

一個進程至少有一個線程,複雜的進程有多個線程。操作系統通過多核cpu快速交替執行這些線程就給人一種同時執行的感覺。

任務隊列

單線程就意味著,所有任務需要排隊,前一個任務結束,後一個任務才會執行。前面的任務耗時過長,後面的任務也得硬著頭皮等待。而任務執行慢通常不是cpu性能不行,而是I/O設備操作耗時長比如Ajax操作從網路讀取數據)。

JS設計者意識到,遇到這種情況主線程可以完全不管I/O設備的結果,先掛起等待結果的任務,然後執行排在後面的任務。直到I/O設備返回了結果,再回過來執行先前掛起的任務。

所以,設計者把電腦的程式任務可以分為兩種,同步任務和非同步任務。同步任務:直接進入主線程執行的任務。前面的任務執行完,後面的才能執行,按順序一個接一個的執行;非同步任務:不會直接進入主線程,而是通過“任務隊列”(task queue)通知“主線程隊列”準備就緒才會進入主線程執行。

具體來說整個機制如下:

  1. 所有同步任務都在主線程上執行,生成一個執行棧(execution context stack)。
  2. 主線程外單獨劃分出一個任務隊列(task queue)。非同步任務在同步任務執行時也不會“偷懶”,同時運行得到結果。然後非同步任務會生成一個對應的通知事件放置於“任務隊列”。
  3. 執行棧中的同步任務執行完畢,系統就會讀取任務隊列中的通知事件,通知事件所對應的非同步任務就會結束等待狀態進入執行棧開始執行。並且通知事件遵循先進先出原則。
  4. 主線程會不斷重覆以上三步。
    機制流程示意圖:
    執行棧一空就會讀取任務隊列,如此往複,這就是JS的運行機制。
    image

事件和回調函數的關係
任務隊列中的通知事件包括了I/O設備事、用戶點擊、頁面滾動等等。只要指定了回調函數(callback)這些事件就會進入任務隊列,等待主線程讀取。

回調函數(callback)的代碼會被任務隊列掛起。所以需要非同步執行某個程式時就請使用回調函數,主線程讀取任務隊列時會先檢查通知事件是否包含【定時器】確認執行時間之類的。

事件迴圈

主線程讀取任務隊列事件是往複迴圈的,整個機制被稱之為事件迴圈(event loop)。
接下來參考Philip Roberts的演講《Help, I'm stuck in an event-loop》深挖事件迴圈
image
從上面的圖示我們能夠看到,主線程執行時產生兩個事物,分別是(heap)和(stack),棧會調用各種外部的WebAPI,它們在"任務隊列"中加入各種事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取任務隊列,依次執行那些事件所對應的回調函數。

定時器[setTimeout]

回過頭來看文章開頭那段代碼

for(var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  });
}
console.log('hello word');

從前面【事件迴圈】小節我們知道了,setTimeout屬於非同步任務,它會生成一個事件(對應指定的回調函數)放進任務隊列掛起,直到中的同步任務都執行完畢後,系統讀取任務隊列拿到通知事件對應的回調函數再放進執行並返回結果。

所以實質上可以看作(取巧方便理解,非實質):

// 同步執行
for(var i = 0; i < 5; i++) {
}
// 同步執行
console.log('hello word');

// 非同步執行
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);

作用域 + 閉包

作用域簡單的說就是js程式當前執行的語境,或者值和表達式可訪問和引用的語境。對象在這個語境中才能才能訪問和引用這個語境中的其他對象。子作用域的對象可以訪問和引用父作用域中的對象,反之不行。
特殊的是一個函數對象在JS中被創建的時候同時創建了閉包閉包是由該函數對象和它所在的語境而構成的一個組合。通常返回一個函數的引用。

// 一個典型的閉包
function makeFunc() {
  var text = "hello world";
  function displayName() {
      console.log(text);
  }
  return displayName;
}
var myFunc = makeFunc();
myFunc();

回過頭來看文章開頭那段代碼,我們就可以利用閉包的原理讓定時器列印出0, 1, 2, 3, 4

for(var i = 0; i < 5; i++) {
  ((i) => {
    setTimeout(function () {
    console.log(i);
	});
  })(i);
}
console.log('hello word');

在上面的代碼中,使用了一個技巧 立即函數 給計時器單獨提供了一個新的作用域,加上裡面的計時器就剛好組成了一個非同步的閉包組合,而且是立刻調用的。

通過上面的手段就可以很好的避免var聲明的迴圈變數暴露在全局作用域帶來的問題。從而列印出0, 1, 2, 3, 4

另外通過let聲明迴圈變數也是很好的解決手段,let允許你聲明一個被限制在塊作用域中的變數、語句或者表達式,這個就是塊級作用域

for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  });
}
console.log('hello word');

let是ES6語法,而塊級作用域的出現解決了var迴圈變數泄露為全局變數的問題和變數覆蓋的問題。

回到上面的代碼,著重說下let是如何做到每次迴圈能夠記憶當前i的值並傳給下次迴圈的:

  1. 首先,在for迴圈中,設置迴圈變數的括弧實質上是一個父作用域,而迴圈體是子作用域。
  2. let聲明瞭該父作用域是塊級作用域而不是全局作用域,每次迴圈i的值只對當前迴圈的塊級作用域有效,就像是塊級作用域是一支捕蟲網,捕獲迴圈更新的i值。迴圈一次就會更新塊級作用域以及變數i,好比拿新的捕蟲網來捕獲新i
  3. 說白了,每次迴圈變數i會重新聲明初始化i。實質上是JS引擎內部會記憶上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。
  4. 迴圈體內部函數會先訪問本身的塊級作用域,沒有i就繼續向上查詢迴圈體作用域,沒有i向上查詢父作用域拿到當前迴圈記憶(捕獲)的i值最後列印出來。
  5. 細心的朋友其實已經發現了,迴圈體內部函數 + 往上查詢的塊級作用域語境剛好組成了類似閉包的組合。

對於不能相容ES6的瀏覽器,我們也可以使用ES5try...catch...語句,形成類似閉包的效果。

for(var i = 0; i < 5; i++) {
  try {
    throw(i)
  } catch(j) {
    setTimeout(function () {
    console.log(j);
	});
  }
}
console.log('hello word');

參考引用:
JavaScript 運行機制詳解:再談Event Loop
阮一峰ES6文檔-let 和 const 命令


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 成本 什麼是成本,即SQL進行查詢的花費的時間成本,包含IO成本和CPU成本。 IO成本:即將數據頁從硬碟中讀取到記憶體中的讀取時間成本。通常1頁就是1.0的成本。 CPU成本:即是讀取和檢測是否滿足條件的時間成本。0.2是每行的CPU成本。 單表查詢計算成本 我們對其進行分析的具體步驟如下: 根據搜 ...
  • 本文記錄如何在 Vue2 環境下儘量使用 Vue3 的 Composition-api 並配合 Vuetify2 使用 ...
  • ⚡工程化、模塊化與更舒服的用戶腳本開發方式,顯著提升開發體驗 ...
  • 第1章 課程介紹(瞭解本課程必看) 試看1 節 | 8分鐘 整體瞭解課程目標和課程內容安排,對 Next.js 作簡要介紹,讓同學對要做的事情有直觀瞭解,準備前置流程。 收起列表 視頻:1-1 導學 (07:23)試看 第2章 使用Next.js 項目初始化及工程配置介紹3 節 | 35分鐘 初始化 ...
  • 第1章 【序章】關於這門課,你需要瞭解得都在這裡 試看3 節 | 5分鐘 前端工程化&ne;Webpack ,真正的前端工程化覆蓋一個項目從創建到開發到發佈的整個流程,既是目前大廠主流的項目提效方案,更是高薪面試 “必考項”!從本章開始,讓我們一起跟隨 Sam 老師,開啟“前端工程化”得升級之旅吧! ...
  • 第1章 課程介紹 試看1 節 | 15分鐘 本章中,將會對課程的內容做介紹說明,總覽課程中涉及到的知識點和學習方向。 收起列表 視頻:1-1 課程介紹 (14:39)試看 第2章 從0搭建一個項目腳手架5 節 | 36分鐘 從0配置開發環境並初始化項目腳手架 收起列表 視頻:2-1 本章導學 (01 ...
  • ###效果圖 ###組件介紹 原生小程式編寫,簡單輕便,拿來即用。 gitee地址:https://gitee.com/qq_connect-EC6BCC0B556624342/wx-calendar ###代碼部分(這裡可能不是最新的推薦去gitee克隆代碼) calendar.wxml <!-- ...
  • 階段一:課程設計及前端創建腳手架開發 第1周 需求分析和架構設計:做什麼,如何做? 開工之前,先來看看我們到底要做一個什麼項目,有哪些功能。然後站在上帝視角,從整體的架構層面,該如何設計該項目。 課程安排: 1、需求分析-瞭解軟體開發生命周期2、技術整體架構3、研發流程優化背後的思考4、為什麼要優化 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...