1.块作用域
尽管函数作用域是最长用的作用域单元,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简介的代码。
分析下面代码:
js
for(var i = 0; i < 10; i++){
console.log(i);
}
在for
循环的头部直接定义了变量i
,通常只想在for
循环内部的上下文中使用i
,但忽略了i
会被绑定在外部作用域中的事实。
这就是块作用域的用处。变量的声明应该距离使用的地方越近越好,并最大限度本地化。
js
var foo = true;
if(foo){
var bar = foo * 2;
console.log(bar); // 2
}
console.log(bar); // 这里也是 2
bar
变量仅在if
声明的上下文中使用,因此如果能将它声明在if
块内部中会很有意义。但是,当使用var
声明的时候,它写在哪里都是一样的,因为它们最终都会属于外部作用域。这段代码是为了风格更易读而伪装出的形式上的块作用域,如果使用这种形式,要确保没在作用域其他地方意外的使用bar
,这只能依靠自觉性。
2 with
with
关键字在前面介绍过,它不仅是一个难于理解的结构,同时也是块作用域的一个例子,用with
从对象中创建出的作用域仅在它内部声明中而非外部作用域有效。
3 try/catch
JavaScript在ES3
中规定try/catch
的catch
分句会创建一个块作用域,其中声明的变量仅在catch
内部生效。
js
try{
undefined(); // 制造错误
}catch (err) {
console.log(err);
}
console.log(err); // 这里就会报错
4 let
在ES6
中引入了新的let
关键字,提供了另一种变量声明方式,let
关键字可以将变量绑定到所在的任意作用域中(通常是{...}内部)。
js
var foo = true;
if(foo){
let bar = foo * 2;
bar = 3;
console.log(bar);
}
console.log(bar); // 这里会报错
用let
将变量附加在一个已经存在的块作用域上的行为是隐式的。
4.1 let 循环
js
for(let i = 0; i < 10; i++) {
console.log(i);
}
for循环头部的let
不仅将i
绑定到了for循环的块中,事实上它将其重新绑定到了循环的每个迭代中,确保使用上一个循环迭代结束时的值进行重新赋值。
js
{
let j;
for(j = 0; j < 10; j++){
let i = j; // 每个迭代重新绑定!
console.log(i);
}
}
由于let
声明附属于一个新的作用域而不是当前作用域,当代码中存在对于函数作用域中var
声明的隐式依赖时,就会有很多隐式陷阱,如果用let
代替var
则需要在代码重构的过程中付出额外的精力。
思考一下:
js
var foo = true;
var baz = 10;
if(foo){
var bar = 3;
if(baz > bar){
console.log(baz);
}
}
这段代码可以简单的被重构为下面代码
js
var foo = true;
var baz = 10;
if(foo) {
var bar = 3;
}
if(baz > bar) {
console.log(baz);
}
但是在使用块级作用域的变量时要注意
js
var foo = true;
var baz = 10;
if(foo) {
let bar = 3;
if(baz > bar) {
console.log(baz);
}
}
如果这时候直接将内部的if
提取出来,就会报错找不到bar
。
5 const
除了let
以外,ES6
中还引入了const
,同样可以用来创建块级作用域,但const
主要用来声明常量。之后的对const
声明的值进行任何修改都会报错。
js
let a = 3;
const b = 5;
a = 4;
b = 6; // 错误
console.log(a); // 4
console.log(b); // 报错
6 小结
函数是JavaScript中最常见的作用域单元,本质上声明一个在函数内部的变量或函数会在所处的作用域中隐藏
起来,但是函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块。