ES6 對let聲明的一點思考

說到ES6的let變量聲明,我估計不少人會想起下面幾個主要的特色:javascript

  • 沒有變量聲明提高
  • 擁有塊級做用域
  • 暫時死區
  • 不能重複聲明

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

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.java

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

不肯意去翻閱資料的就看我下面的我的總結吧。git

變量聲明提高

關於變量聲明提高,有幾個重點:es6

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

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

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?,將這個過程描述得很清晰:ide

// 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?

相關文章
相關標籤/搜索