ES6 系列之 let 和 const
mqyqingfeng opened this issue · comments
块级作用域的出现
通过 var 声明的变量存在变量提升的特性:
if (condition) {
var value = 1;
}
console.log(value);
初学者可能会觉得只有 condition 为 true 的时候,才会创建 value,如果 condition 为 false,结果应该是报错,然而因为变量提升的原因,代码相当于:
var value;
if (condition) {
value = 1;
}
console.log(value);
如果 condition 为 false,结果会是 undefined。
除此之外,在 for 循环中:
for (var i = 0; i < 10; i++) {
...
}
console.log(i); // 10
即便循环已经结束了,我们依然可以访问 i 的值。
为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。
块级作用域存在于:
- 函数内部
- 块中(字符 { 和 } 之间的区域)
let 和 const
块级声明用于声明在指定块的作用域之外无法访问的变量。
let 和 const 都是块级声明的一种。
我们来回顾下 let 和 const 的特点:
1.不会被提升
if (false) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
2.重复声明报错
var value = 1;
let value = 2; // Uncaught SyntaxError: Identifier 'value' has already been declared
3.不绑定全局作用域
当在全局作用域中使用 var 声明的时候,会创建一个新的全局变量作为全局对象的属性。
var value = 1;
console.log(window.value); // 1
然而 let 和 const 不会:
let value = 1;
console.log(window.value); // undefined
再来说下 let 和 const 的区别:
const 用于声明常量,其值一旦被设定不能再被修改,否则会报错。
值得一提的是:const 声明不允许修改绑定,但允许修改值。这意味着当用 const 声明对象时:
const data = {
value: 1
}
// 没有问题
data.value = 2;
data.num = 3;
// 报错
data = {}; // Uncaught TypeError: Assignment to constant variable.
临时死区
临时死区(Temporal Dead Zone),简写为 TDZ。
let 和 const 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错:
console.log(typeof value); // Uncaught ReferenceError: value is not defined
let value = 1;
这是因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。
看似很好理解,不保证你不犯错:
var value = "global";
// 例子1
(function() {
console.log(value);
let value = 'local';
}());
// 例子2
{
console.log(value);
const value = 'local';
};
两个例子中,结果并不会打印 "global",而是报错 Uncaught ReferenceError: value is not defined
,就是因为 TDZ 的缘故。
循环中的块级作用域
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 3
一个老生常谈的面试题,解决方案如下:
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = (function(i){
return function() {
console.log(i);
}
}(i))
}
funcs[0](); // 0
ES6 的 let 为这个问题提供了新的解决方法:
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
问题在于,上面讲了 let 不提升,不能重复声明,不能绑定全局作用域等等特性,可是为什么在这里就能正确打印出 i 值呢?
如果是不重复声明,在循环第二次的时候,又用 let 声明了 i,应该报错呀,就算因为某种原因,重复声明不报错,一遍一遍迭代,i 的值最终还是应该是 3 呀,还有人说 for 循环的
设置循环变量的那部分是一个单独的作用域,就比如:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
这个例子是对的,如果我们把 let 改成 var 呢?
for (var i = 0; i < 3; i++) {
var i = 'abc';
console.log(i);
}
// abc
为什么结果就不一样了呢,如果有单独的作用域,结果应该是相同的呀……
如果要追究这个问题,就要抛弃掉之前所讲的这些特性!这是因为 let 声明在循环内部的行为是标准中专门定义的,不一定就与 let 的不提升特性有关,其实,在早期的 let 实现中就不包含这一行为。
我们查看 ECMAScript 规范第 13.7.4.7 节:
我们会发现,在 for 循环中使用 let 和 var,底层会使用不同的处理方式。
那么当使用 let 的时候底层到底是怎么做的呢?
简单的来说,就是在 for (let i = 0; i < 3; i++)
中,即圆括号之内建立一个隐藏的作用域,这就可以解释为什么:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
然后每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。这样对于下面这样一段代码
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
就相当于:
// 伪代码
(let i = 0) {
funcs[0] = function() {
console.log(i)
};
}
(let i = 1) {
funcs[1] = function() {
console.log(i)
};
}
(let i = 2) {
funcs[2] = function() {
console.log(i)
};
};
当执行函数的时候,根据词法作用域就可以找到正确的值,其实你也可以理解为 let 声明模仿了闭包的做法来简化循环过程。
循环中的 let 和 const
不过到这里还没有结束,如果我们把 let 改成 const 呢?
var funcs = [];
for (const i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // Uncaught TypeError: Assignment to constant variable.
结果会是报错,因为虽然我们每次都创建了一个新的变量,然而我们却在迭代中尝试修改 const 的值,所以最终会报错。
说完了普通的 for 循环,我们还有 for in 循环呢~
那下面的结果是什么呢?
var funcs = [], object = {a: 1, b: 1, c: 1};
for (var key in object) {
funcs.push(function(){
console.log(key)
});
}
funcs[0]()
结果是 'c';
那如果把 var 改成 let 或者 const 呢?
使用 let,结果自然会是 'a',const 呢? 报错还是 'a'?
结果是正确打印 'a',这是因为在 for in 循环中,每次迭代不会修改已有的绑定,而是会创建一个新的绑定。
Babel
在 Babel 中是如何编译 let 和 const 的呢?我们来看看编译后的代码:
let value = 1;
编译为:
var value = 1;
我们可以看到 Babel 直接将 let 编译成了 var,如果是这样的话,那么我们来写个例子:
if (false) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
如果还是直接编译成 var,打印的结果肯定是 undefined,然而 Babel 很聪明,它编译成了:
if (false) {
var _value = 1;
}
console.log(value);
我们再写个直观的例子:
let value = 1;
{
let value = 2;
}
value = 3;
var value = 1;
{
var _value = 2;
}
value = 3;
本质是一样的,就是改变量名,使内外层的变量名称不一样。
那像 const 的修改值时报错,以及重复声明报错怎么实现的呢?
其实就是在编译的时候直接给你报错……
那循环中的 let 声明呢?
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
Babel 巧妙的编译成了:
var funcs = [];
var _loop = function _loop(i) {
funcs[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
funcs[0](); // 0
最佳实践
在我们开发的时候,可能认为应该默认使用 let 而不是 var ,这种情况下,对于需要写保护的变量要使用 const。然而另一种做法日益普及:默认使用 const,只有当确实需要改变变量的值的时候才使用 let。这是因为大部分的变量的值在初始化后不应再改变,而预料之外的变量之的改变是很多 bug 的源头。
ES6 系列
ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。
哈,还是先写了 ES6 ,手动 DOGE。
期待V8源码系列已经很久了,我的大刀已经饥渴难耐了!
for (var i = 0; i < 3; i++) {
var i = 'abc';
console.log(i);
}
// abc
这里为什么会只输出一个abc呢??求教
@brushbird
'abc' ++ --> NaN
NAN<3 --->False
循环退出
非常赞。
@BeijiYang 写完 ES6 系列,等我写 React 系列的时候,就可以尽情的使用 ES6 的语法 (๑•̀ㅂ•́)و✧
@CharlyCheng 这个……不是想让你伤心……但我没有说过写 V8 系列……
@shadowprompt @thisisandy @horizon0514 @JChermy 感谢你们,送上致意 o(////▽////)q
let 在 for 循环中创建的隐藏作用域,以及babel模拟创建函数作用域,用以保存i的值,让我印象深刻呀。以前只知道for循环中let可以解决var变量问题,现在知道原理了
我就是来刷刷存在感滴
@brushbird 'abc'<3 -> false
博主有个问题不太理解~
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); }
babel 编译后为
for (var i = 0; i < 3; i++) { var i = "abc"; console.log(i); }
这快我有点不理解 编译前后的代码 执行行为应该是一致的 现在却不一致.
@lzy68187311 你确定编译后是这个样子?应该不是吧
@heyunjiang ( ̄▽ ̄)~*
@liuxinqiong ( ̄∇ ̄)
有两个疑问:
var value = "global";
// 例子1
(function() {
debugger // 在断点处访问 value,返回的是 undefined
console.log(value);
let value = 'local';
}());
这个例子中,自执行函数中
1、先执行 console.log(value),此时还没有执行 let value = 'local';
那么 value 是怎么被放入 TDZ 的呢?
2、如果加上了 debugger,在断点处访问 value,返回的是 undefined,又是为什么?
Babel 巧妙的编译成了
应该为巧妙地
有两个疑问:
var value = "global";
// 例子1
(function() {
debugger // 在断点处访问 value,返回的是 undefined
console.log(value);
let value = 'local';
}());
这个例子中,自执行函数中
1、先执行 console.log(value),此时还没有执行 let value = 'local';
那么 value 是怎么被放入 TDZ 的呢?
2、如果加上了 debugger,在断点处访问 value,返回的是 undefined,又是为什么?
同问临时死区的概念分析?
@mqyqingfeng ,js 深入系列时,有人要说分析V8源码来,好像后来再看,已经从入门到放弃了
@CharlyCheng js引擎会先扫描整个代码,在自执行函数作用域内扫描到了变量定义(let value = 'local'),导致value被加入死区。之后执行过程中在函数作用域内访问value,则报错
不要V8,但说好的React系列呢。。
我的项目babel编译下面这段代码也有问题
// 编译前
for (let i = 0; i<3;i++) {
let i = 10
console.log(i) //10,10,10
}
// 编译后
for (var i = 0; i < 3; i++) {
var i = 10;
console.log(i); //10
}
全局安装的babel-cli@6.26.0,以及项目下安装的babel-preset-env@^1.7.0, babel-preset-stage-2@^6.24.1
//.babelrc
{
"presets": [
["env", {
"targets": {
"browsers": ["last 3 versions", "> 2%", "ie >= 9", "Firefox >= 30", "Chrome >= 30"]
},
"modules": false,
"loose": true,
"useBuiltIns": true
}],
"stage-2"
]
}
有人遇到和我一样的问题么
babel处理for中的let的方法,可以说是一种非常优雅的解决办法了
小鸡蛋里挑骨头:
最后一段而预料之外的变量*之*的改变是很多 bug 的源头
,错别字🤭
nice 马飞
大佬 就是我遇到一个解构赋值的地方想请教一下
function ListNode(val) {
this.val = val;
this.next = null;
}
var head = new ListNode(1)
var tmp = head
for (var i = 2; i < 5; i++) {
head.next = new ListNode(i)
head = head.next
}
// console.log(tmp)
var swapPairs = function (head) {
// console.log(head)
if (!head || !head.next) return head
let tmp = head.next
console.log(tmp)
[head.next, tmp.next] = [swapPairs(tmp.next), head] // 1
// head.next = swapPairs(tmp.next) //2
// tmp.next = head //3
return tmp
}
console.log(swapPairs(tmp))
1 和 2+3有什么区别呢,感觉看起来一样,但是答案似乎有出入...
for (var i = 0; i < 3; i++) { var i = 'abc'; console.log(i); } // abc这里为什么会只输出一个abc呢??求教
因为for( )里面使用var时没有在( )内形成封闭的作用域,i被循环体赋值成'abc',!!('abc' < 3) === false,循环停止了。
终于等到你~~~~~~~~~~~~
@mqyqingfeng
有个疑惑,我看到JavaScript深入之变量对象这一章,里面说所有的变量和函数声明都会放到变量对象中,变量提升就是访问这个变量对象(不知道这里理解的对不对),那let 和const 声明的变量有没有放进来呢?
是放进来了然后还把它们放到TDZ声明中,所以造成暂时性死区。
还是没有放进来,ES5和ES6的代码分析规则不同。
在看国外的帖子里,说到let和const是有变量提升的,官方有没有一个明确的定义呢。
@ldsyzjb
我觉得let和const是有变量提升的,
像下面这个例子,
报错出现在color = "yellow"
这句上,
假如没有变量提升,color会声明在全局对象里,
所以显然引擎已经处理了后面的let声明,将color变量加入了TDZ里了
补充一下:这种提升只会在块级作用域内部
@ldsyzjb
我觉得let和const是有变量提升的,
像下面这个例子,
报错出现在color = "yellow"
这句上,
假如没有变量提升,color会声明在全局对象里,
所以显然引擎已经处理了后面的let声明,将color变量加入了TDZ里了
补充一下:这种提升只会在块级作用域内部
代码执行之前,js引擎会扫描代码,在扫描函数体的时候,发现有let的定义,就会把这个let声明的变量放到临时死区(TDZ)内,所以会报 "color is not defined";
如果像你补充的,存在块级作用域变量提升的话,那console出来的应该时red,后边的赋值覆盖掉前边的
大佬,默默的偷偷的问一下,react系列准备写么
感谢感谢,文章看了很有收获,相同的知识点,博主分析更升入,也更有体系,看了也有新收获,很棒很棒,博主就是知识大厨!
想问下就是在for in循环中const不会报错
这一块,有说到:
每次迭代不会修改已有的绑定,而是会创建一个新的绑定
这里的绑定
是什么意思?
@BeijiYang TYUT?是太原理工的同学么?
@ldsyzjb @zhw2590582 let/const声明变量的时候是没有变量提升的。你们使用的那个例子已经说明拉,在函数当中对 color 进行赋值,但是报错了,这是因为不会对 let 声明的变量进行提升,而是将这个变量放到 TDZ 当中,只有执行到声明语句后,才能对这个变量进行其他的赋值操作~
for (var i = 0; i < 3; i++) { var i = 'abc'; console.log(i); } // abc这里为什么会只输出一个abc呢??求教
输出一个原因是第一次进入for循环内部时,i的值被改变成了字符串‘abc’,执行第一遍结束时执行i++,此时i的值就是NaN,第二次循环跟3比较时,返回false,所以只执行了一次。所以只打印了一个
什么时候写vue系列呢 一个学vue的小白的诉求
想知道let a=3
在函数执行时创建上下文时,在AO活动对象中是什么状态,初始化的时候a是直接不存在,还是已经声明但是没有初始化undefined
干货满满的,能转载到我的博客吗?会注明链接
不会被提升为什么是这个例子呢?这个例子说明的是let块级作用域吧?而且let是会被hoist的
if (false) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
var funcs = [], object = {a: 1, b: 1, c: 1};
for (var key in object) {
funcs.push(function(){
console.log(key)
});
}
funcs[0]()
这个 为神魔是c 呢,和下面这个是同一个原因吗
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 3
一次循环后i = 'abc' 已经不满足 i < 3的条件啦 @wudixiaodoujie
for (var i = 0; i < 3; i++) { var i = 'abc'; console.log(i); } // abc这里为什么会只输出一个abc呢??求教
上面不是有人已经说过了吗,i = abc ,然后再++ 转化成 NaN ,小于 3
突发奇想,babel如何解决命名冲突,因为babel在for循环中使用let,会生成一个叫做 _loop
的变量,但是,如果把这个变量提前声明占位会发生什么呢? 结论是 _loop
会变为 _loop2
正常情况
// 编译前Original case
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
// 编译后Babel case
var funcs = [];
var _loop = function _loop(i) {
funcs[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
不正常情况
下面为提前申明 _loop
,原来的本该变为_loop的变量,此时命名为_loop2
// 编译前Original case
var funcs = [];
var _loop = 1; //提前定义 _loop 占位,看babel如何解决冲突
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
// 编译后Babel case
var funcs = [];
var _loop = 1; //提前申明的变量依旧在
// 原来的本该变为_loop的变量,此时命名为_loop2
var _loop2 = function _loop2(i) {
funcs[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 10; i++) {
_loop2(i);
}
感谢楼主
for (var i = 0; i < 3; i++) { var i = 'abc'; console.log(i); } // abc这里为什么会只输出一个abc呢??求教
上面不是有人已经说过了吗,i = abc ,然后再++ 转化成 NaN ,小于 3
我也是这样想的,但是当执行"abc"++
的时候,会报错的啊,并不能得出NaN
。
@brushbird 'abc'<3 -> false
当执行完var i = "abc" console.log(i)
时,下一步应该会执行i++
吧,那终止的条件不能是"abc" < 3
-> false 啊。验证了一下,是在i++
这一步终止循环。按理说执行"abc"++
时会报错的,怎么没有呢🤔
js里面的所有声明都是有提升的,也是看过了您的关于变量对象的文章才得出此结论,提升的本质应该是当前变量在变量对象初始化时,创建于变量对象中。把变量对象的变化过程分为三个步骤,第一步是创建,第二步是初始化,第三步是赋值。对于var/class/function/形参等创建和初始化在变量对象的初始化阶段,对于function、class、形参等赋值也在变量对象的初始化阶段。而let、const在变量对象的初始化阶段是被创建但未初始化的。
@ZhangDaZongWei 你好,在 for
循环中执行的代码是 var i = "abc"; i++
。而你直接是把字符串带入 到++
操作了,这两者是不一样的。报错是因为 ++
操作符只能用在左值上吧,也就是变量上。例如:i++
其实等价于 i = i + 1
。若咱们直接使用 "abc"++
,那就等同于 "abc" = "abc" +1
,但是 "abc"
是一个字符串常量,它不能放在 =
左边的。所以直接在浏览器上执行 "abc"++
是会报错的。个人理解,请多指教~
const 声明不允许修改绑定,但允许修改值
这句话我觉得有一丝歧义,const 其实就是不可修改值,比如一个对象
const obj = {
name: ''
}
obj 指向的是变量在内存中的地址,这个地址(也就是const声明的标识符的值)不可以被修改,但是这个地址指向的内存空间,也就是name所在的空间所存储的值,是可以随意修改的。
const 声明不允许修改绑定,但允许修改值
这句话我觉得有一丝歧义,const 其实就是不可修改值,比如一个对象
const obj = { name: '' }
obj 指向的是变量在内存中的地址,这个地址(也就是const声明的标识符的值)不可以被修改,但是这个地址指向的内存空间,也就是name所在的空间所存储的值,是可以随意修改的。
其实这个就是跟按值传递同一个类型的问题
我觉得 let const 声明的变量是会提升的,只是没有初始化,所以访问报错(未初始化前不能访问)
可以这么理解:
function test() {
// TDZ 开始
color = "yellow";
let color = "red"; // TDZ 结束,在 let 命令声明变量 color 之前,都属于变量 color 的“死区”。
console.log(color);
}
所以,color = "yellow" 这里就会报错了。我一开始以为报错是在打印语句那,懵逼了半天。
至于为什么有人说还是会变量提升,其实是受到了块级作用域的干扰。在块级作用域和在全局作用域下,下面的代码表现会不一致,如下:
在全局作用域下,的确是不存在变量提升的,在块级作用域下,貌似是存在类似于变量提升的现象,区别在于未初始化不能访问。
react 的什么时候出呀
关于 let 提升的问题,可以看看这篇方老大的总结, 至于结论,没有这篇文章精彩
声明
var变量函数提升放到变量环境,let const放在词法环境。想多了解看yygmind/blog#12
其他资料,极客时间 浏览器原理与实践专栏
@brushbird 'abc'<3 -> false
当执行完
var i = "abc" console.log(i)
时,下一步应该会执行i++
吧,那终止的条件不能是"abc" < 3
-> false 啊。验证了一下,是在i++
这一步终止循环。按理说执行"abc"++
时会报错的,怎么没有呢🤔
i='abc'
i++ //NAN
求教 for (let i = 0; i < 3; i++) { console.log(i) } 的伪代码应该是什么样子,如果按这个形式
{ let i = 0 { console.log(i) } i++ { console.log(i) } i++ { console.log(i) } i++ }
可以解释一些问题,比如
(1) 循环体内可以重复声明循环条件中定义的变量
for (let i = 0; i < 3; i++) { let i = 1 }
// 不报错, 循环条件 与 循环体 并非同一个作用域,而是嵌套关系
(2) 循环体内可以改变循环条件中的变量
for (let i = 0; i < 3; i++) { i = 10; console.log(i) }
// 只执行了第一次,执行体内对变量的修改影响了执行条件中的同名变量,再次证明嵌套关系
(3) const 无法声明循环条件的变量
for (const i = 0; i < 3; i++) { console.log(i) }
// 由此可见循环条件内存在对变量 i 对修改,即 i++
但是无法解释以下情况
(4) for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i) }, 0) }
@617429782 在for循环里有特殊处理,可以搜一下知乎方应杭的写的一篇文章,名字我记得大概是终于理解了let
react啥时候出大佬
for (var i = 0; i < 3; i++) { var i = 'abc'; console.log(i); } // abc这里为什么会只输出一个abc呢??求教
var i=abc
你说呢
请问一下,tdz具体是个啥东西,是会专门开辟一段内存去储存let const这些吗
块级作用域存在于 “块中(字符 { 和 } 之间的区域)”, 这句怎么理解呀
function init () {
console.log('init called');
return Math.random();
}
for (let i = 0, j = init(); i < 100; i++) {
console.log(i);
console.log(j);
}
// 最后输出的j都是完全一样的,for循环的第一段变量声明应该只会执行一次,但是会单独开辟一个块级作用域
解决了之前一些模棱两可的问题,感谢~