在函数作用域或全局作用域中通过关键字var声明变量,但是无论在哪里声明,都会被当成在当前作用域顶部声明的变量,这就是提升(Hoisting)机制。
运行如下代码,输出如右。
console.log(x); // -> undefined var x = 5; console.log(x); // -> 5 console.log(y); // -> Uncaught ReferenceError: y is not defined
原因
在预编译阶段,JavaScript引擎在运行代码前,重新调整代码结构,提升变量x的声明,修改成如下
var x; console.log(x); // -> undefined x = 5; console.log(x); // -> 5 console.log(y); // -> Uncaught ReferenceError: y is not defined
为此,ECMAScript 6 引入块级作用域来强化对变量声明周期的控制。
块级声明
块级声明用于 声明 在指定块的作用域之外无法访问 的 变量。块级作用域存在于:
- 函数内部
- 块中(字符{和}之间的区域)
let声明
用let代替var来声明变量,就可以把变量的作用域限制在当前代码块中。
禁止重声明
假设作用域中已经存在某个标识符,再用let关键字声明,它就会报错。
同一作用域中不能用let重复定义已经存在的标识符
var a = 20; // 抛出语法错误 let a = 40;
但是如果当前作用域内嵌另一个作用域,便可以在内嵌的作用域中用let声明同名变量, 如下所示.
var a = 20; // 不会抛出错误 if(condition){ let a = 40; }
if块内声明的变量a会遮蔽全局作用域中的a, 全局作用域的a只能在if块外才能访问到.
const声明
使用const声明的是常量, 其值一旦被设定后不可更改. 因此, 每个通过const声明的常量必须进行初始化.
- const与let声明的都是块级标识符, 所以常量也只有在当前代码块内有效, 一旦执行到块外会被立即销毁.
- 与let相似, 在同一作用域用const声明已经存在的标识符也会导致语法错误.
- 无论在严格模式还是非严格模式下, 都不可以为const定义的常量再赋值.
- JavaScript中的常量如果是对象, 则对象的值可以修改
针对以上四点中的最后两点举例说明:
const x = 5; x = 6;// 抛出语法错误
当用const声明对象时
const person = { name: 'Lili' }; // 修改对象属性的值 person.name = 'Sue'; // 成功修改 // 如下操作会抛出语法错误 person = { name: 'Sue' };
临时死区(Temporal Dead Zone)
最常见的例子是:
var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; }
上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。
临时死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
也通过临时死区, 发现了之前的知识盲点:
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
临时死区部分参考博客:
循环中的块作用域绑定
使用var, 由于var声明得到了提升,变量i在循环结束后仍可访问。
for(var i = 0; i < 10; 1++){ process(items[i]) } // 这里仍可访问i console.log(i); // ->10
使用let,变量i只存在于for循环中。
for(let i = 0; i < 10; 1++){ process(items[i]) } // 这里不可访问i, 抛出一个错误。 console.log(i);
循环中的函数
var items=[]; for(var i = 0; i < 10; i++){ items.push(function(){ console.log(i); }); } items.forEach(function(item){ item(); });
这是因为循环里的每次迭代同时共享着变量i,循环内部创建的函数全都保留了对相同变量的引用,循环结束的时候变量i的值为10,所以每次调用console.log(i)
时,就会输出数字10
如果一定要用var,在循环中使用立即调用函数表达式(IIFE),以强制生成计数器变量的副本。
输入:var items=[]; for(var i = 0; i < 10; i++){ items.push((function(value){ return function(){ console.log(value); } }(i))); } items.forEach(function(item){ item(); });
在循环内部,IIFE表达式为接受每一个变量i都创建了一个副本并存储为变量value。
循环中的let声明
let声明模仿上述示例中IIFE所做的一切来简化循环过程,每次迭代循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。
var items=[]; for(let i = 0; i < 5; i++){ items.push(function(){ console.log(i); }); } items.forEach(function(item){ item(); });
每次循环的时候,let声明都会创建一个新变量i,并将其初始化为i的当前值,所以循环内部创建的每个函数都能得到属于它们自己的i的副本。
对于for-in循环和for-of循环来说也是一样的。
var items=[]; var object={ a:1, b:2, c:3, }; for(let key in object){ items.push(function(){ console.log(key); }); } items.forEach(function(item){ item(); });
let声明在循环内部的行为是标准中专门定义的,它不一定与let的不提升特性相关。实际上,早期let实现不包含这一行为,它是后来加入的。
循环中的const声明
ES6中没有明确指明不允许在循环中使用const声明,然而针对不同类型的循环const会表现出不同的行为。
对于普通的for循环来说,可以在初始化变量的时候使用const,但是更改这个变量就会抛出错误。
var items=[]; for(const i = 0; i < 10; i++){ items.push(function(){ console.log(i); }); }
在这段代码中,i被声明为常量。在循环的第一次迭代中,i=0,迭代执行成功。然后执行i++,该语句试图更改常量i,因此抛出错误。
对于for-in循环和for-of循环中,使用const时的行为来说也是一样的。
var items=[]; var object={ a:1, b:2, c:3, }; for(const key in object){ items.push(function(){ console.log(key); }); } items.forEach(function(item){ item(); });
这段代码与“循环中的let声明”里描述for-in循环的代码几乎一模一样,唯一区别就是在循环内不能改变key的值
之所以const可以用在for-in和for-of中,是因为每次迭代不会修改已有的绑定,而是创建一个新的绑定。
全局块作用域绑定
当var被用于全局作用域时,它会创建一个新的全局变量作为全局对象的属性。这意味着var很有可能会无意中覆盖一个已经存在的全局属性。
网友评论