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是有变量声明提升的啊,这个发现引起了我的兴趣。我立马去找了一些相关的资料查看,在查看的过程中,我也慢慢了解了其他一些隐含的容易误解的知识点,下面罗列一些相关资料,方便让有同样兴趣了解的童鞋去查阅:
- 关于对let变量声明的:
ES6 In Depth: let and const
Are variables declared with let or const not hoisted in ES6? - js变量作用域概念,比较基础
What is the scope of variables in JavaScript? - 这一篇文章也很基础,从作用域,上下文,this,执行上下文,闭包,立即执行函数,等等都讲了一遍,稍微提到了词法作用域(lexical scope)即静态作用域。
Understanding Scope in JavaScript - 这里通过例子解释词法作用域(lexical scope),很容易理解
What is lexical scope? - 对于for循环中let的表现说明
Why is let slower than var in a for loop in nodejs? - 这个很多东西都讲到了,也涉及let的作用域到for循环,不过文章好长,我只看了相关到部分。
You Don't Know JS: Scope & Closures
不愿意去翻阅资料的就看我下面的个人总结吧。
变量声明提升
关于变量声明提升,有几个重点:
- 所有的变量声明( 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
并没有重新声明。看来我们有必要借助其他文档来帮助理解。
MDN上的文档,提到for循环中,每进入一次花括号就生成了一个块级域,即每个循环进入函数体的
i
都绑定到了不同的块级域中,由于是不同的块级作用域,所以每次进入循环体函数的i
值都相互独立,不是同一个作用域下的值。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.
每一个闭包(即循环体函数)会捕获循环变量的不同副本,而不是都捕获同一个循环变量。这里说明了循环体函数中的循环变量不是简单的引用,而是一个副本。
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
参考文档:
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
What is the scope of variables in JavaScript?
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声明的一点思考的更多相关文章
- c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程
c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...
- 关于java异常的一点思考
关于异常的一点思考 异常生命周期 异常的来源 所有的异常都是抛出来的 有底层api抛出的 有自定义抛出的 异常的处理 1, 运行时异常 不做任何处理仍可编译通过 不建议捕获(不建议用异常来做流程控制, ...
- MSSQL显错注入爆数字型数据的一点思考
Title:MSSQL显错注入爆数字型数据的一点思考 --2011-02-22 15:23 MSSQL+ASP 最近在弄个站点,密码是纯数字的,convert(int,())转换出来不报错,也不知道其 ...
- 对dump脱壳的一点思考
对dump脱壳的一点思考 偶然翻了一下手机日历,原来今天是夏至啊,时间过的真快.ISCC的比赛已经持续了2个多月了,我也跟着比赛的那些题目学了2个月.......虽然过程很辛苦,但感觉还是很幸运的,能 ...
- 关于linux kernel slab内存管理的一点思考
linux kernel 内存管理是个很大的话题,这里记录一点个人关于slab模块的一点思考总结. 有些书把slab介绍成高速缓存,这会让人和cache,特别是cpu cache混淆,造成误解.sla ...
- es6系列-变量声明
es6系列所有文章都是阅读阮一峰老师的<ES6标准入门>(第2版)所做的读书笔记.方便日后查阅相关基础知识. git地址: https://github.com/rainnaZR/es6- ...
- 关于html页面元素语义化的一点思考
这几天在看招聘公告前端工程师的要求基本都附带了html语义化的要求,所以稍微关注了下这方面的知识.对于其中的一点就是要求页面元素在去除css样式之后还能有良好的布局引发了我一点思考.作为前端刚入门的我 ...
- 基于CAS分析对ABA问题的一点思考
基于CAS分析对ABA问题的一点思考 什么是CAS? 背景 synchronized加锁消耗太大 volatile只保证可见性,不保证原子性 基础 用CPU提供的特殊指令,可以: 自动更新共享数据; ...
- 【翻译】全球用尽IPv4的一点思考
作者:Dimple 公众号:奔跑吧攻城狮 简介:专属于Java和Android开发,和你聊聊职场话题,一同展望未来 作为小小号主的我表示很无力啊,这几天,天天都是热点.前有网易员工勇敢发声维护自己的利 ...
随机推荐
- 在Signalr的Hub中写方法实现与安卓的数据交互
简介: 实现数据实时刷新:SignalR 后台服务:.NET/WebAPI 为了减轻web的压力,将接口中接收数据的方法写到SignalR的Hub中 在此放一小段代码给自己加深下印象,博主有点健忘.. ...
- progID
ProgID程序员给CLSID指定的容易记住的名字ProgID命名约定:<Program>.<Component>.<Version>AppID:将某个APPID( ...
- TypeScript设计模式之单例、建造者、原型
看看用TypeScript怎样实现常见的设计模式,顺便复习一下. 单例模式 Singleton 特点:在程序的生命周期内只有一个全局的实例,并且不能再new出新的实例. 用处:在一些只需要一个对象存在 ...
- Django之Model世界
Model 到目前为止,当我们的程序涉及到数据库相关操作时,我们一般都会这么搞: 创建数据库,设计表结构和字段 使用MySQLdb 来连接数据库,并编写数据访问层代码 业务逻辑层去调用数据访问层执行数 ...
- SQL SERVER - 谈死锁的监控分析解决思路
1 背景 1.1 报警情况 最近整理笔记,打算全部迁移到EVERNOTE.整理到锁这一部分,里边刚好有个自己记录下来的案例,重新整理分享下给大家. 某日中午,收到报警短信,DB死锁异常,单分钟死锁12 ...
- PetaPoco 快速上手
今天来给大家分享一个好用的轻型的.net框架的ORM——PetaPoco 本着快速上手的原则,我们通过和EF对比,让大家能快速使用PetaPoco PetaPoco大家可能没有听说过,但大家一定听说过 ...
- 我的第一个webapiDemo
最近在面试,总是被问到有没有接触过webapi 或者问webapi和MVC 的区别,今天看了看,原来如此,自己理解感觉webapi和webservice一样像是实现soa 的一种形式,也可以理解为服务 ...
- requireJS的配置心得
1.html页面中如果同时存在data-main和require()和配置(config中的baseUrl),那么定义根路径 baseUrl > data-main > index.htm ...
- Redis-port安装使用实现redis迁移codis,以及简单redis pipe实现将mysql迁移redis
(0)Redis-port原理: 首先是看到下面这篇文档开始研究的redis-port http://www.itnpc.com/news/web/146085373656602.html 简要截图 ...
- SQL语句流程函数
本人因为今天用到了流程函数,顿时感觉语法生疏啊,为了防止以后忘记,故写此篇!!! 流程函数是MySQL相对常用的一类函数, 用户可以使用这类函数在一个SQL语句中实现条件选择, 这样能够提高效率. 下 ...