顶层const和底层const的理解_ES6学习之let和const

点击上方蓝色字体轻松关注

在我们开发的时候,可能认为应该默认使用 let 而不是 var ,这种情况下,对于需要写保护的变量要使用 const。然而另一种做法日益普及:默认使用 const,只有当确实需要改变变量的值的时候才使用 let。这是因为大部分的变量的值在初始化后不应再改变,而预料之外的变量之的改变是很多 bug 的源头。我们来看下背后的是什么鬼!

基础理论

块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域存在于:

  • 函数内部
  • 块中(字符 { 和 } 之间的区域)

let 和 const 都是块级声明的一种。

我们来回顾下 let 和 const 的特点:

  1. 不会被提升
if (false) {    let value = 1;}console.log(value); // Uncaught ReferenceError: value is not defined
  1. 重复声明报错
var value = 1;let value = 2; // Uncaught SyntaxError: Identifier 'value' has already been declared
  1. 不绑定全局作用域

当在全局作用域中使用 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命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;if (true) {  tmp = 'abc'; // ReferenceError  let tmp;}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

typeof x; // ReferenceErrorlet x;

上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。

作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

typeof undeclared_variable // "undefined"

上面代码中,undeclared_variable是一个不存在的变量名,结果返回“undefined”。所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。

循环中的块级作用域

var a = [];for (var i = 0; i < 10; i++) {  a[i] = function () {    console.log(i);  };}a[6](); // 10

一个常见的面试题,解决方案如下:

var a = [];for (var i = 0; i < 10; i++) {    a[i] = (function(i){        return function() {            console.log(i);        }    }(i))}a[6](); // 6

ES6 的 let 为这个问题提供了新的解决方法:

var a = [];for (let i = 0; i < 10; i++) {  a[i] = function () {    console.log(i);  };}a[6](); // 6

问题在于,上面讲了 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

为什么结果就不一样了呢,如果有单独的作用域,结果应该是相同的呀……,这是因为在 ECMAScript 规范第 13.7.4.7 节 http://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation

for 循环中使用 let 和 var,底层会使用不同的处理方式。

那么当使用 let 的时候底层到底是怎么做的呢?

简单的来说,就是在 for (let i = 0; i < 3; i++) 中,即圆括号之内建立一个隐藏的作用域,

然后每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。这样对于下面这样一段代码

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 a = [];for (const i = 0; i < 10; i++) {    a[i] = function () {        console.log(i);    };}a[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

后记

以上就是小编今天给大家分享的内容,喜欢的小伙伴记得收藏转发,点击在看推荐给更多的小伙伴。

大前端实践者,专注于大前端技术领域,分享前端系统架构,框架实现原理,最新最高效的技术实践!

关注大前端实践者,学习前端不迷路,欢迎多多留言交流...

大前端实践者

06434beafc79b73efa587bf8cc94a4e7.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值