ES6(let & const)-01篇

块级作用域

if(condition) {
  var value = 1;
}
console.log(value);

初学者可能认为只有当condition为true的时候,才会创建value,如果condition为false,结果应该报错,然而因为变量提升代码相当于:

var value;
if(condition) {
  value = 1;
  console.log(value);
}

如果condition为false,结果为underfined
还有一个经典的例子for循环里:

for(var i =0; i<10 ; i++) {
  ...
}
console.log(i);//10

即使for循环已经结束,i还是可以访问。

为了加强对变量生命周期的控制,ECMAScript6引入的块级作用域。
块级作用域存在于:

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

if(true) let x = 1; 会报错,应写成if(true) {let x = 1}

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 value =1;
console.log(window.value);//undefined

再来说下 let 和 const 的区别:
onst 用于声明常量,其值一旦被设定不能再被修改,否则会报错。

值得一提的是:const 声明不允许修改绑定,但允许修改值。这意味着当用 const 声明对象时:

const data = {
  value: 1
}
//没有问题
data.value = 2;
//报错
data = {} ;
//Uncaught TypeError: Assignment to constant variable.

临时性死区

临时性死区(Temporal Dead Zone),简称TDZ
let 和const声明的变量不会被提升到作用域的顶部,如果实在声明之前访问这些变量,会导致报错。

console.log(value);
let value = 1;
// Uncaught ReferenceError: value is not defined

这是因为Javascript引擎在扫描变量的时候,要么将他们提升到作用域顶部(var),访问TDZ众多的变量会出发运行时的错误,只有执行变量声明之后才会从TDZ中移出,然后才能访问。

我们来理解下

var value = "global";
//例子一
(function() {
  console.log(value);
  let value = "local";
}())
//例子二
{
  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
funcs[1](); //1

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

var funcs = [];
for(let i=0; i< 3; i++) {
  function[i] = function() {
    console.log(i);
  }  
}

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

(i=abc;abc++ --> NaN; NaN<3--> false 跳出循环)
为什么结果就不一样了呢,如果有单独的作用域,结果应该是相同的呀……

如果要追究这个问题,就要抛弃掉之前所讲的这些特性!这是因为 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 vaue = 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 在 for 循环中创建的隐藏作用域,以及babel模拟创建函数作用域,用以保存i的值.
参考文章:
阮一峰-let 和 const 命令
原文详见:
ES6 系列之 let 和 const
又是学习的一天( ̄∇ ̄)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。