[ES6] 从 var 到 let

函数作用域(Function Scope)与声明提前(Hoisting)

在一些类似于C语言的编程语言中,每一对花括号 {} 都是一个作用域,变量只在其被声明时所在的作用域内有效,我们称之为块级作用域(Block Scope)

在ES5标准下的Javascript中,没有块级作用域,取而代之地使用了函数作用域(Function Scope): 变量在声明它们的函数体,以及函数体嵌套的任意函数体内都是有定义的。

以上的定义意味着,函数内声明的所有变量,无论其在函数内声明的位置,只要在函数体内部,都是可见的,这个特性被称为声明提前(Hoisting),即Javascript函数里声明的所有变量(不包括其赋值)都被提前至函数体的顶部。

注:声明提前这步操作是在Javascript引擎预编译时进行的,是在代码开始运行之前。

[ES6] 从 var 到 let

这种机制的缺点

缺点1:内层变量可能会覆盖外层变量

请看下面的例子

[ES6] 从 var 到 let

以上代码的问题在于,即使condition为false的情况下,变量a仍然会被声明,且可以在else中被访问到(此时由于未被赋值,其值为undefined)。这样会在实际的开发中如果不留心,可能导致内层变量会覆盖外层变量的问题。如:

[ES6] 从 var 到 let

缺点2:for循环中用于计数的变量会泄露为全局变量

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

在此想多介绍一下Javascript中的作用域

ES5标准下Javascript中只有两种作用域,全局作用域和函数作用域,直观来说:

[ES6] 从 var 到 let

那么请看如下代码:

{
 var a = 0;
}
console.log(a); //0

这里a虽然在花括号内,但不在函数内部,则依然属于全局作用域,任何地方均可以访问到变量a。

那我们再看for循环,在for循环中

[ES6] 从 var 到 let

看清楚,for循环并不是在函数作用域,而是在全局作用域进行的。这样一来,循环变量就会一直存在于全局作用域中,造成隐患。

只拥有全局作用域和函数作用域而导致的问题随着Javascript程序规模的扩大日益凸显出来,于是在ES6标准中,加入了块作用域,用let关键字声明的变量的作用域为块作用域。具体来说:

  1. 用let声明的变量其作用域以花括号为界,即块作用域(Block Scope)。
  2. 用let声明的变量不存在声明提前(Hoisting)。

例子1:

{
 var a = 'A';
 let b = 'B';
}
console.log(a); // A
console.log(b); // ReferenceError: b is not defined

例子2:

function test(){
 console.log(a); //undefined (声明提前)
 console.log(b); //ReferenceError: b is not defined (不存在声明提前)
 var a = 'A';
 let b = 'B';
}
test();

在for循环中,计数变量特别适合用let来声明,避免污染全局变量。如:(请对比前文使用var的for循环例子)

for(let i = 0; i < 10; i++){
 //...
}
console.log(i); //ReferenceError: i is not defined

临时死区

再进一步,由于用let声明的变量不存在声明提前,在一个变量如果用了let声明,那么从当前块作用域开始处( “{” 位置),到变量声明处,就形成了一个该变量名的临时死区(temporal dead zone)。如

function test(){
 
 //变量tag的临时死区
 let tag = 'B';
}

临时死区可能会带来的问题:

[ES6] 从 var 到 let

很明显,第三行代码位于tag的临时死区内。因此对于名字为tag变量的所有操作都会报错。有些临时死区造成的错误比较隐蔽,如:

[ES6] 从 var 到 let

再比如:

[ES6] 从 var 到 let

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。例如:

[ES6] 从 var 到 let

长久以来,var声明让开发者在循环中创建函数变得异常困难,因为变量到了循环之外仍能被访问的到,例如:

[ES6] 从 var 到 let

我们预期的输出结果是输出0-9,但此段代码实际上输出了十次数字10。

这是因为循环里的每次迭代同时共享着变量i,循环内部创建的函数全部保留了对相同变量的引用,循环结束时i的值为10,所以每次调用console.log(i)时都会输出数字10

------《深入理解ES6》

我们来深究一下这个问题。

console.log(i) 中的 i 是可以被改变的,并不是在函数定义后就固定不变的。

[ES6] 从 var 到 let

Javascript是基于词法作用域的,词法作用域的基本规则是:

Javascript函数执行用到了作用域链,这个作用域链是在 >>>函数定义的时候

什么意思呢?

当我们定义一个函数时,系统实际保存了:

  1. 函数的定义
  2. (函数定义所在的作用域) 的 所有变量
  3. (函数定义所在的作用域) 的 (父作用域) 的 所有变量
  4. (函数定义所在的作用域) 的 (父作用域) 的 (父作用域) 的 所有变量
  5. ...
  6. 直到最顶层的全局作用域

其中 2~6 形成了一个作用域链。我们可以简单说:

当我们定义一个函数时,系统实际保存了:

  1. 函数的定义
  2. 函数定义时所在的作用域链 (再次提醒,是函数定义时,而不是函数执行时)

当函数执行时,用到一个变量,则按照从2至6的顺序查找变量名,把最先找到的拿来执行。而如果最后在6(也就是全局变量才查找到)

[ES6] 从 var 到 let

当我们 调用函数A之前,函数A定义之后,第五行的 var i = 3 中的 i 并没有机会被修改。这是因为每一层函数都形成了一个闭包,即函数内部的变量保存在函数作用域内,外部并访问不到。

那么再看如下例子:

[ES6] 从 var 到 let

这里在 调用函数A之前,函数A定义之后,i 的值可以被任意修改。

这样我们在回到刚刚的for循环中定义函数的代码:

[ES6] 从 var 到 let

i是一个在全局作用域的变量,在每次循环结束后都会被加1,聪明的WebStorm知道这是在for循环中容易犯的错误,于是抛出警告,告诉你这样写会和你本意相悖,输出十个10,而不是你想要的0,1,2,3,4,5,6,7,8,9。

那么如何解决这个问题呢?在ES6标准之前,开发者们在函数中使用立即调用表达式(IIFE),强制生成计数器变量的副本,来保证最后输出结果是0到9:

[ES6] 从 var 到 let

在ES6中,只要使用let关键字来声明循环变量 i ,就可以方便的实现:

[ES6] 从 var 到 let

最后说说const

熟悉其他语言的同学可能会对const并不陌生,在ES6中,它与let的主要区别是:

  1. const声明一个常量,在声明时就必须初始化,之后不可更改
  2. 若用const声明一个对象,对象整体不能修改,但可以修改对象中属性的值。
[ES6] 从 var 到 let

好了,就先介绍到这里,觉得不错别忘了点赞呦,比心❤️。


分享到:


相關文章: