本文内容
本文记录一些关于 JavaScript 中块级作用域的内容
var 声明及变量提升机制
在函数作用域或者全局作用域通过 var 声明的变量,无论在哪里声明的,实际上都会被当成当前作用域顶部声明的变量
function getValue(condition) {
if (condition) {
var value = "blue";
return value;
} else {
// 此处可以访问变量 value,其值为 undefined
return null;
}
// 此处可以访问变量 value,其值为 undefined
}
注:如果我们访问一个从没声明过的变量,会抛出错误。而如果访问一个声明但是没有赋值的变量,会访问到 undefined。
上述代码实际上被 JavaScript 引擎替换成了如下代码:
function getValue(condition) {
var value;
if (condition) {
var value = "blue";
return value;
} else {
return null;
}
}
变量 value 的声明,被提升到了当前作用域的顶部。
块级作用域
ES6 添加了块级作用域,块级作用域和其它语言的块级作用域很像了,存在于:
- 函数内部
- 块中(大括号之间的区域)
let 声明
let 声明的语法和 var 相同。但是 let 声明可以把变量的作用域限制在当前代码块中。let 声明的变量不会有变量提升,而且变量存在于块级作用域中,就比较像是其它语言常规的声明的作用域了。
function getValue(condition) {
if (condition) {
let value = "blue";
return value;
} else {
// 此处 value 不存在,访问将会报错
return null;
}
// 此处 value 不存在,访问将会报错
}
禁止重复声明
如果当前作用域已经存在了某个标识符,使用 let 再次声明的话,将会抛出错误
var s = 1;
let s = 2;
输出:
(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
/Users/czp/workspace/knowledge-base/demos/node_start/hello.js:2
let s = 2;
^
SyntaxError: Identifier 's' has already been declared
但是如果当前作用域内嵌了另一个作用域,便可以在内嵌作用域中使用 let 声明了,例如
var s = 1;
{
let s = 2;
}
这种方式,可以理解为变量遮蔽。
const 声明
使用 const 声明的是常量,其值一旦被设定后就无法修改。因此每个通过 const 声明的变量,必须同时进行初始化。
const a = 3;
// 错误 没有进行初始化
const b;
const 声明的也是个具有块级作用域的变量(常量)。与 let 的性质一样,如果当前作用域已经存在了某个标识符,使用 const 再次声明会报错。但是如果当前作用域嵌套了内部作用域,在内部的作用域中可以使用 const 再次声明常量,这也是一种变量遮蔽
const 声明的常量,只是指指针不可变,如果 const 声明的是一个对象,那么对象内部的值是可以改变的(一大堆面向对象语言里的常量都有这种特性,就不细说了)
临时死区(TDZ)
临时死区这个概念,其实主要是用来描述 let 和 const 的变量不提示的效果的,实际上来说,let 和 const 声明的变量,除了不会提升之外,还有一些细微的区别,参考下面两种代码的区别
typeof a;
const a = 3;
输出:
(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
/Users/czp/workspace/knowledge-base/demos/node_start/hello.js:1
typeof a;
^
ReferenceError: Cannot access 'a' before initialization
如果不使用 const 声明 a 的话:
typeof a;
将不会报错,因为 typeof 操作符操作一个未声明过的变量时,值是 undefined,而不会直接报错
以上两种代码的例子,可以说明,let 和 const 的变量不提示除了不提示之外还有别的副作用。
实际上,使用 let 和 const 声明的变量,在 JavaScript 运行到它们声明的那一行之前,都处于一个特殊的区域中,称之为临时死区。当我们在 let 和 const 声明语句之前访问它们时,会访问到临时死区的变量,就会抛出错误。
JavaScript 引擎在扫描到变量声明时,要么将其进行作用域提升,要么将其放入到作用域的临时死区中,访问临时死区的变量将会抛出错误。
如果我们在临时死区对应的作用域外面访问该变量,则不会出错
typeof a;
{ const a = 3; }
这里的临时死区实际上属于 {} 里面,所以上面的 typeof a 访问的是个未定义变量,不会有错误抛出
循环中的块级绑定
for(let i = 0;i < 10;i++){
;
}
// 这里将不可以访问 i 了
console.log(i);
这个就很类似于其它语言的块级作用域了,讲道理的话循环结束后本来就不应该还能继续访问循环里定义的变量(只能说 ES 6 终于把这个实现的和正常的语言一样了)
循环中的函数
我们可以通过循环里生成函数闭包来体验一下 var 和 let 在循环中的区别
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function () {
console.log(i);
})
}
funcs.forEach(function (func) {
func();
});
上述代码将会打印 10 个 10,至于为什么,我们已经在之前介绍闭包的时候讲过了:
- 外层函数生成 Call 对象
- 从头到尾只有同一个变量 i
- 闭包里引用的都是同一个 i
- 因此最后都输出了 i 的终值 10
这里主要是因为 i 变量从头到尾只有一个,所以闭包里引用的都是同一个值
下面看 let 声明的方式:
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs.push(function () {
console.log(i);
})
}
funcs.forEach(function (func) {
func();
});
上述代码将会打印 0 到 9。
主要是因为:每次迭代的时候,都会生成一个新的值,第一次迭代的 i 和第二次迭代的 i 在内存中不是同一个对象了。
实际上这种特性是单独定义的,专门存在于循环当中。
循环中的 const 声明
const 声明在不同的循环类型中的表现是不一样的,如下是 for 循环
var funcs = [];
for (const i = 0; i < 10; i++) {
funcs.push(function () {
console.log(i);
})
}
funcs.forEach(function (func) {
func();
});
将会报错,因为在循环中对 const 进行了修改
而在 for-in 和 for-of 循环中,表现与 let 类似
var funcs = [];
var obj = {
a: 1,
b: 2,
c: 3
};
for(const key in obj){
funcs.push(function(){
console.log(key);
})
}
funcs.forEach(function(func){
func();
})
上述代码将会打印 a b c
与上面关于循环中的函数一节相似,与其中 let 声明的表现一致,唯一的区别是循环中不能改变 const 的值
全局块作用域绑定
当我们在全局作用域下的时候,使用 var 声明变量就会为全局对象绑定新属性,例如在浏览器环境中
var a = 3
console.log(window.a)
a 会被挂载到全局对象 window 上 (node 下不会有这种行为,因为 node 中我们代码都运行在模块里,是个闭包)
但是使用 let 和 const 声明的变量,将不会被挂载到全局对象下面
let a = 3
console.log(window.a === a) // false
所以如果我们不想给全局对象赋值或者不小心将属性给覆盖掉了的话,使用 let 和 const 要保险很多
最佳实践
一句话:优先用 const、其次是 let,最后是 var
其实就是说,大部分的值我们都不需要修改,就行 scala 里面,基本都是优先用 val,很少用 var 一样。
只有确实需要修改变量采用 let