ES6 對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

本文地址:http://www.cnblogs.com/JuFoFu/p/6726359.html

水平有限,錯誤歡迎指正。原創博文,轉載請注明出處。


參考文檔:

阮一峰 . 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?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM