ES6 系列之 let 和 const

简介: 通过 var 声明的变量存在变量提升的特性,初学者可能会觉得只有 condition 为 true 的时候,才会创建 value,如果 condition 为 false,结果应该是报错,然而因为变量提升的原因。

1.png


块级作用域的出现


通过 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 节:


2.png


我们会发现,在 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 系列目录地址:github.com/mqyqingfeng…


ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。


如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。



目录
相关文章
|
1月前
ES6中的var,let,const
ES6中的var,let,const
|
1月前
|
安全 JavaScript 前端开发
ES6 中 let 与 const命令
ES6 中 let 与 const命令
|
7天前
|
前端开发 JavaScript 开发者
ES6 标准之 let、const 详解
ES6 为JavaScript带来了许多强大的新特性,使开发者能够编写更加简洁、清晰和高效的代码。let和const作为新的变量声明方式,提供了更严格的作用域控制和不可变性,有助于减少错误和提高代码质量。
55 2
ES6 标准之 let、const 详解
|
1月前
|
JavaScript IDE 开发工具
es6学习笔记(一)let、const
es6学习笔记(一)let、const
|
10月前
|
JavaScript 前端开发 安全
ES6(let和const命令)
ES6(let和const命令)
53 0
|
自然语言处理 JavaScript 前端开发
每天3分钟,重学ES6-ES12(二)var let const的选择
每天3分钟,重学ES6-ES12(二)var let const的选择
72 0
|
JavaScript 前端开发
ES6——let、const
ES6——let、const
96 0
|
JavaScript 前端开发
【ES6】 let与const详解
【ES6】 let与const详解
|
JavaScript
ES6中let与const的区别
ES6中let与const的区别