JS基础(二)块作用域

欢迎阅读,本篇主要介绍JS中的块作用域.

前言

尽管上篇讲到的函数是最常见的作用域单元,本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件设计原则。但函数不是唯一的作用域单元,其他类型作用域是存在的,通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简洁的代码。

什么是块作用域?

块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块,通常指 {} 内部。

for () { }

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

相信大家对上述代码非常熟悉,我们在for循环的头部直接定义变量 i,通常是因为只想在for循环内部的上下文中使用 i,而忽略了 i 会绑定在外部作用域(函数或全局)中的事实。这就是块级作用域的用处。变量的声明应该距离使用的地方越近越好,并最大限度地本地化。块作用域是一个用来对之前 最小授权 原则进行扩展的工具,将代码从函数中隐藏信息扩展为在块中隐藏信息。

with 关键字

with 关键字也是块级作用域的一种形式,用 with 从对象中创建的作用域仅在 with 声明中而非外部作用域中有效,由于 with 关键字并不常用且不推荐使用,再此不做详细讨论。

try/catch

很少有人注意到JS的ES3规范中规定 try / catch 的 catch 分句会创建一个块级作用域,其中声明的变量仅在 catch 内部有效。

1
2
3
4
5
6
7
try {
undefined(); // 执行一个非法操作来制造异常
} catch (err) {
console.log(err); // 能够正常执行
}

console.log(err); // err not found

尽管这个行为已经被标准化了,而且被大部分的标准JS环境所支持,但是当同一作用域中的两个或多个 catch 分句用同样的标识符名称声明时,很多静态检查工具(并不是全部)还是会发出警告,实际上这并不是重复定义,因为所有变量都被安全地限制在块作用域内部,为了避免不必要的警告,我们一般会将 catch 的参数命名为 err1、err2 等,或者关掉检查工具。

let

ES6 引入了 let 关键字,提供了除了 var 之外的另一种变量声明方式。let 关键字可以将变量绑定到所在的任意作用域中(通常是 {…}内部)。或者说,let 为其声明的变量隐式的“劫持”了所在的块作用域。

1
2
3
4
5
6
7
8
var foo = true;

if (foo) {
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
console.log(bar); // ReferenceError

用 let 将变量附加在一个已经存在的块级作用域上的行为是隐式的。在开发中,如果我们没有特意关注哪些作用域中绑定了哪些变量,并且习惯性移动或将这些块包含在其他块里,就会导致代码变得混乱。为作用域显示的创建块可以部分解决这个问题,使代码的附属关系变得更加清晰,通常来讲,显式的代码优于隐式或一些精巧但不清晰的代码。

1
2
3
4
5
6
7
8
9
10
var foo = true;

if (foo) { // <-- 显示的块
{
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
}
console.log(bar); // ReferenceError

只要是声明有效,在声明中的任意位置都可以使用 {…} 括号来为 let 创建一个用于绑定的块,我们在 if 声明内部显式的创建了一个块,如果需要对其进行重构,整个块都可以被方便地移动而不会对外部 if 声明的位置和语义产生任何影响。
需要说明的是,在我们的项目中,显示创建块级作用域({…})需要和团队成员在写法、分块原则、代码易读性上达成共识后再使用,使用不当,反而会让代码变得难读懂、难维护,所以你可能会看到有人反而不推荐此种写法。

const

除了 let 之外,ES6 还引入了 const,同样可以用来创建块作用域变量,其值是固定的常量。之后任何试图修改值的操作都会引起错误。

1
2
3
4
5
6
7
8
9
10
11
12
var foo = true;

if (foo) {
var a = 2;
const b = 3; // 包含在 if 中的块作用域常量

a = 3; // 正常
b = 4; // 错误
}

console.log(a); // 3
console.log(b); // ReferenceError

总结

从ES3开始,try / catch 结构在 catch 分句中具有块作用域。
从ES6中引入了 let 关键字用来在任意代码块中声明变量。if () { let a = 2; } 会声明一个劫持了 if 的 {…} 块的变量,并将变量添加到这个块中。
块级作用域不应该完全作为函数作用域的代替方案,两种功能应该同时存在,开发者可以并且应该根据需要选择使用哪种作用域,创造可读、可维护的优良代码。

下篇计划学习 - 提升。感谢一起学习。