说到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?

水平有限,错误欢迎指正。原创博文,转载请注明出处。

ES6 深入let的作用域的更多相关文章

  1. ES6之块级作用域

    一.前言 在ECMAScript6(以下简称ES6)之前,ECMAScript的作用域只有两种: 1.  全局作用域: 2.  函数作用域. 正是因为有这两种作用域,所以在JavaScript中出现一 ...

  2. ES6(块级作用域)

    我们都知道在javascript里是没有块级作用域的,而ES6添加了块级作用域,块级作用域能带来什么好处呢?为什么会添加这个功能呢?那就得了解ES5没有块级作用域时出现了哪些问题. ES5在没有块级作 ...

  3. JavaScript 一、 ES6 声明变量,作用域理解

    // JavaScript/* * ========================================================= * * 编译原理 * 尽管通常将 JavaScr ...

  4. 【读书笔记】【深入理解ES6】#1-块级作用域绑定

    var声明及变量提升(Hoisting)机制 在函数作用域或全局作用域中通过var关键字声明的变量,无论实际上是在哪里声明的,都会被当成在当前作用域顶部声明的变量.这就是我们常说的提升(Hoistin ...

  5. es6声明对象以及作用域与es5对比

    es6声明变量: let x=1;//声明一个变量 const y=2;//声明一个只读常量,声明时必须赋值,之后值不可修改 es5声明变量: var z=3;//声明一个变量 区别: let不存在变 ...

  6. ES6 - Note1:块级作用域与常量

    在ES6以前,ES不支持块级作用域,只有全局作用域和函数作用域,所有变量的声明都存在变量声明提升. 1.let 关键字 声明一个块级变量,只在一个代码块中有效,如果在块外面访问便会报错,如下所示: { ...

  7. ES6 学习笔记之二 块作用域与闭包

    "闭包是函数和声明该函数的词法环境的组合." 这是MDN上对闭包的定义. <JavaScript高级程序设计>中则是这样定义的:闭包是指有权访问另一个函数作用域中的变量 ...

  8. ES6的 let const 以及块级作用域

    let声明变量 用法类似于var,但是所声明的变量只在let所在的代码块内有效. 1 . 在ES6环境下,let声明的变量不能在声明之前调用. 例: console.log(i); //会报错,这叫做 ...

  9. ES6 块级作用域

    作用域包括:全局作用域,函数作用域,块级作用域. 为什么要用块级作用域: 1.内层变量可能会覆盖外层变量. var name = "kevin"; function call() ...

随机推荐

  1. 【openstack N版】——云主机调整大小\冷迁移

    一.先决条件 云主机冷迁移,即:将一台云主机从一个计算节点上,迁移到另外一个计算节点上.因为环境原因,所以我们需要准备两个计算节点. 1.1准备环境 在控制节点上,安装一个计算节点 #在控制节点安装n ...

  2. 利用smarty模板(登录、有关信息操作等功能)

    smarty模板前提是:前端和后端是分开的,所以肯定会有很多的后台页面,php页面和html页面是分开存储的. (1)登录页面的编写也是分两个页面(后台和前端) 1.首先是后台的php页面,很简单只要 ...

  3. web计时机制——performance对象

    前面的话 页面性能一直都是Web开发人员最关注的领域.但在实际应用中,度量页面性能的指标,是提高代码复杂程度和巧妙地使用javascript的Date对象.Web Timing API改变了这个局面, ...

  4. Day01 Java环境变量配置

    1. Java环境配置的确浪费了一些时间,网上找的资料在设置PATH.CLASSPATH几乎都是利用的JAVA_HOME的路径 例如CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_ ...

  5. 基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2

    来个目录吧: 第一章 第二章 第三章 暂时就这么多.后面路线更新吧 Entity Framework Core Nuget包管理 如果你创建项目的时候启用了个人身份验证的话,项目中就已经包含了EFCo ...

  6. Isomorphic Strings leetcode

    Given two strings s and t, determine if they are isomorphic. Two strings are isomorphic if the chara ...

  7. OpenStack/devstack with Neutron on Ubuntu 14 (2)

    在前面的文章中,已经完成了devstack的安装.下面,我会介绍如何使用neutron 首先创建两个neutron net, vmnet1 和vmnet2 stack@ubuntu:~/devstac ...

  8. 【SSH项目实战三】脚本密钥的批量分发与执行

    [SSH项目实战]脚本密钥的批量分发与执行 标签(空格分隔): Linux服务搭建-陈思齐 ---本教学笔记是本人学习和工作生涯中的摘记整理而成,此为初稿(尚有诸多不完善之处),为原创作品,允许转载, ...

  9. Dark的项链(树链剖分)

    P2272 - Dark的锁链 Description 无向图中有N个节点和两类边,一类边被称为主要边,而另一类被称为附加边.Dark有N – 1条主要边,并且Dark的任意两个节点之间都存在一条只由 ...

  10. ArrayList去除重复元素(包括字符串和自定义对象)

    1.去除重复字符串 package com.online.msym; import java.util.ArrayList; import java.util.Iterator; @SuppressW ...