ES6 對let聲明的一點思考

来源:http://www.cnblogs.com/JuFoFu/archive/2017/04/18/6726359.html
-Advertisement-
Play Games

說到ES6的 變數聲明,我估計很多人會想起下麵幾個主要的特點: 沒有變數聲明提升 擁有塊級作用域 暫時死區 不能重覆聲明 很多教程和總結基本都說到了這幾點(說實話大部分文章都大同小異,摘錄的居多),習慣性我還是去看了MDN上的文檔,立馬發現一個問題: In ECMAScript 2015, let ...


說到ES6的let變數聲明,我估計很多人會想起下麵幾個主要的特點:

  • 沒有變數聲明提升
  • 擁有塊級作用域
  • 暫時死區
  • 不能重覆聲明

很多教程和總結基本都說到了這幾點(說實話大部分文章都大同小異,摘錄的居多),習慣性我還是去看了MDN上的文檔,立馬發現一個問題:

In ECMAScript 2015, let will hoist the variable to the top of the block. However, referencing the variable in the block before the variable declaration results in a ReferenceError. The variable is in a "temporal dead zone" from the start of the block until the declaration is processed.

ECMAScript 2015(即ES6),let會提升變數到代碼塊頂部。然而,在變數聲明前引用變數會導致ReferenceError錯誤。在代碼塊開始到變數聲明之間變數處於暫時死區(temporal dead zone)。
不得了,看來let是有變數聲明提升的啊,這個發現引起了我的興趣。我立馬去找了一些相關的資料查看,在查看的過程中,我也慢慢瞭解了其他一些隱含的容易誤解的知識點,下麵羅列一些相關資料,方便讓有同樣興趣瞭解的童鞋去查閱:

不願意去翻閱資料的就看我下麵的個人總結吧。

變數聲明提升

關於變數聲明提升,有幾個重點:

  • 所有的變數聲明( var, let, const, function, function*, class)都存在變數聲明提升,我們這裡只談論let變數
  • let被提升到了塊級作用域的頂部,表現(或者說換種說法)就是每個let定義的變數都綁定到了當前的塊級作用域內。通俗地講,因為塊級作用域在頂部就為每個let定義的變數留好了位置,所以只要在let變數聲明前引用了這個變數名,塊級作用域都會發現並拋出錯誤
  • var的變數聲明提升會將變數初始化為undefined,let沒有初始化,所以有暫時死區的概念。其實從表現上來講,說let是沒有變數聲明提升也有一定道理,因為變數沒有在頂部初始化,所以也不能說變數已經聲明過了,反而用綁定到了當前的塊級作用域內這種說法更令人信服

在我的思路大概清晰寫這篇總結的時候,我又偶然在一篇講變數聲明提升的博文上看到一段MDN原文的引用:

In ECMAScript 6, let does not hoist the variable to the top of the block. If you reference a variable in a block before the let declaration for that variable is encountered, this results in a ReferenceError, because the variable is in a "temporal dead zone" from the start of the block until the declaration is processed.

納尼!居然和我現在看到的MDN文檔不一致......博文的日期是2015-06-11,看來這個概念也在改變,與時俱進啊。既然如此,我覺得也沒有必要深究了,因為不管概念怎麼變,只要能夠知道let在塊級作用域的正確表現就可以了,理論還是要為實踐服務。

let在for迴圈中的表現

for的運行機制

說到for迴圈,先說明下for的運行機制,比如說for(var i=0;i<10;i++){...}即先初始化迴圈變數(var i=0),這一句只運行一次,然後進行比較(i<10),然後運行函數體{...},函數體運行結束後,如果沒有break等跳出,再運行自增表達式(i++),然後進行比較判斷(i<10)是否進入執行體。下麵是引用別人的一個回答How are for loops executed in javascript?,將這個過程描述得很清晰:

// for(initialise; condition; finishediteration) { iteration }
var initialise = function () { console.log("initialising"); i=0; }
var condition = function () { console.log("conditioning"); return i<5; }
var finishediteration = function () { console.log("finished an iteration"); i++; }
var doingiteration = function () { console.log("doing iteration when `i` is equal", i); }
for (initialise(); condition(); finishediteration()) {
    doingiteration();
}

initialising
conditioning
doing iteration when `i` is equal 0
finished an iteration
conditioning
doing iteration when `i` is equal 1
finished an iteration
conditioning
doing iteration when `i` is equal 2
finished an iteration
conditioning
doing iteration when `i` is equal 3
finished an iteration
conditioning
doing iteration when `i` is equal 4
finished an iteration
conditioning

for迴圈中的let

之所以要單獨講for迴圈中的let,是因為看到了阮老師ES6入門中講let的那一章的一個例子:

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

對這個例子原文中是這樣的解釋的:

上面代碼中,變數i是let聲明的,當前的i只在本輪迴圈有效,所以每一次迴圈的i其實都是一個新的變數,所以最後輸出的是6。你可能會問,如果每一輪迴圈的變數i都是重新聲明的,那它怎麼知道上一輪迴圈的值,從而計算出本輪迴圈的值?這是因為 JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。

JavaScript 引擎內部會記住上一輪迴圈的值這句解釋我覺得作為程式猿估計怎麼都無法認可吧?記住這個詞說得太模糊了,其中固然有某種機制或規範。而且每一輪迴圈的變數i都是重新聲明,那麼下麵的例子就難以解釋:

for (let i = 0; i < 5; i++){
    i++;
    console.log(i)
}
// 1
// 3
// 5

如果迴圈函數體內的i每次都是重新聲明的,那麼函數體內即子作用域內改變i的值,為什麼能夠改變外層定義的i變數?
再來看文中提的另外一個例子:

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

這個例子原文的解釋是:

迴圈語句部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。

如果按照上面的邏輯每個子作用域內的i都重新聲明,那麼在同一個子作用域內為什麼能夠二次聲明?
很明顯,i並沒有重新聲明。看來我們有必要藉助其他文檔來幫助理解。

  1. MDN上的文檔,提到for迴圈中,每進入一次花括弧就生成了一個塊級域,即每個迴圈進入函數體的i都綁定到了不同的塊級域中,由於是不同的塊級作用域,所以每次進入迴圈體函數的i值都相互獨立,不是同一個作用域下的值。

  2. ES6 In Depth: let and const文章中是這樣解釋的:

    each closure will capture a different copy of the loop variable, rather than all closures capturing the same loop variable.

    每一個閉包(即迴圈體函數)會捕獲迴圈變數的不同副本,而不是都捕獲同一個迴圈變數。這裡說明瞭迴圈體函數中的迴圈變數不是簡單的引用,而是一個副本。

  3. You Don't Know JS: Scope & Closures 中的理解:

    Not only does let in the for-loop header bind the i to the for-loop body, but in fact, it re-binds it to each iteration of the loop, making sure to re-assign it the value from the end of the previous loop iteration.

    let 不僅在頭部將i值綁定到for迴圈體中,事實上,let將i重新綁定到每個迭代函數中,並確保將上一次迭代結束的結果重新賦值給i

這裡提到的子作用域(for迴圈的函數體{...}),其實準確地講叫詞法作用域(lexical scope),也被稱為靜態作用域。簡單地講就是在嵌套的函數組中,內部函數可以訪問父作用域的變數和其他資源。

結合上面的幾點可知,子作用域內用的還是外層聲明的i變數,let i = 'abc';就相當於在子作用域中聲明新的變數覆蓋了父作用域的變數聲明。但是子作用域內引用的這個父作用域變數不是直接引用,而是父作用域變數的一個副本,子作用域修改這個副本時,相當於修改父作用域變數,而父作用域迴圈變數改變時,不會影響子作用域內的副本變數,加粗的這句解釋說實話還是沒能說服我自己,所以我又找到了stackoverflow上的一個回答。

Why is let slower than var in a for loop in nodejs?雖然不是正面回答for迴圈的問題,但是裡面舉的一個Babel實現let的例子卻能從var的角度來解釋這個問題:

"use strict";
(function () {
  var _loop = function _loop(_j) {
    _j++; // here's the change inside the new scope
    setTimeout(function () {
      console.log("j: " + _j + " seconds");
    }, _j * 1000);
    j = _j; // here's the change being propagated back to maintain continuity
  };
  for (var j = 0; j < 5; j++) {
    _loop(j);
  }
})();

仔細看這個例子,外層定義的j變數由形參_j(這裡的形參傳值,就是動態作用域)傳入了迴圈體函數_loop()中,進入函數體中後,_j就相當於他的副本,子作用域可以修改父作用域變數(表現在 j = _j),但_loop()函數執行結束後,父作用域變數j的修改無法改變_loop()函數中的形參_j,因為形參_j只會在_loop()函數執行那一次被賦值,後面外層j值的修改和他沒有關係。回想一下上面的問題,如果內部重新定義了j值,那麼就會覆蓋外層傳進來的_j(雖然在這個例子里j_j變數名不一樣,但是在let聲明裡其實是同一個變數名),相當於子作用域定義了自己內部使用的變數,j = _j;這樣的賦值語句也沒有意義了,因為這相當於變數自己給自己賦值。

上面這段話是從var實現let的角度來解釋,有點拗口。下麵說說我的理解,談談let變數是怎麼處理這個過程的:
for迴圈每次進入函數體{...}中,都是進入了新的子作用域中,每個子作用域相互獨立,新的子作用域引用(實際是變數複製)父作用域的迴圈值變數,同時可以修改變數的值且更新父作用域變數,實際表現就和真正引用了父作用域變數一樣。反之,父作用域無法訪問此複製變數,所以父作用域中變數的改變不會對子作用域中的變數有什麼影響。但是如果子作用域中重新聲明瞭此變數名,新的變數就綁定到了子作用域中,變成了子作用域的內部變數,覆蓋了父作用域的迴圈值變數,子作用域對新聲明的變數的修改都在子作用域範圍內,父作用域同樣無法訪問此變數。

小結

明白這些概念有時候感覺很繁雜,好像有點牛角尖,但是我覺得只有掌握正確的理解方向,才能夠根據實際情況去推斷、讀懂代碼,也有利於自己寫出規範化、易理解的代碼。這篇文章的內容依然是我理解思路的一個記錄,有點啰嗦,主要為了以後自己概念模糊後能夠找到現在思考的思路,由於其中有很多自己的理解,錯漏在所難免,也希望大家讀後能給我提出意見和建議。



本文來源:JuFoFu

本文地址:




參考文檔:

阮一峰 . let和const命令

Jason Orendorff . ES6 In Depth: let and const

You-Dont-Know-JS . You Don't Know JS: Scope & Closures

Hammad Ahmed . Understanding Scope in JavaScript

MDN let

MDN for...of

What is the scope of variables in JavaScript?

What is lexical scope?

Why is let slower than var in a for loop in nodejs?

Are variables declared with let or const not hoisted in ES6?

水平有限,錯誤歡迎指正。原創博文,轉載請註明出處。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1269 匈牙利游戲 2012年CCC加拿大高中生信息學奧賽 1269 匈牙利游戲 2012年CCC加拿大高中生信息學奧賽 時間限制: 1 s 空間限制: 128000 KB 題目等級 : 鑽石 Diamond 時間限制: 1 s 空間限制: 128000 KB 題目等級 : 鑽石 Diamond ...
  • 軟體開發模型:1.瀑布模型1)軟體概念階段 用戶需求2)需求分析 軟體需求3)架構設計 架構文檔4)詳細設計 模型設計5)編碼階段 代碼文檔6)測試階段瀑布模型的特點是在每個階段的工作都清晰詳盡,容易預估風險和開發成本,每個階段人員安排也非常清晰。瀑布模型的缺點是中途不能出現任何問題,例如客戶要改動 ...
  • 請允許我用22種編程語言,向各位親們祝福…… C:printf("祝大家聖誕節快樂"); C++ : cout<<"祝大家聖誕節快樂"; QBasic : Print "祝大家聖誕節快樂" Asp : Response.Write "祝大家聖誕節快樂" PHP : echo "祝大家聖誕節快樂"; ...
  • 對於RBAC的一些思考 RBAC : Role Based Access Control 基於角色的訪問控制 問:引入 的目的是什麼? 答: 1. 在沒有role的時候,要決定一個角色有沒有許可權我們必須把角色和許可權綁定起來,引入role是為瞭解耦角色和許可權。 2. 解耦的好處表現在角色和許可權的變化比 ...
  • spring的MVC執行原理 優點: 1.使用Spring的IOC容器,將對象之間的依賴關係交給Spring,降低組件之間的耦合性,讓我們更專註於應用邏輯 2.可以提供眾多服務,事務管理,WS等。 3.AOP的很好支持,速食麵向切麵編程。 4.對主流的框架提供了很好的集成支持,如Hibernate, ...
  • 在gulpfile.js里添加var ejs = require('gulp-ejs') 命令行中執行: npm install gulp-ejs gulp.task('gulp-ejs', function(){ gulp.src(模版目錄 + '/**/*.html') .pipe(data(f ...
  • display || visibility list style : list style type || list style position || list style image position top || right || bottom || left z index clear fl ...
  • <!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <style> /*跟運動相關的模塊都需要絕對定位*/ #idv1 {background: #CCCCCC;display: none;width: 50px;p ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...