我们在 JavaScript 词法作用域不完全指北 中介绍了词法作用域,词法作用域是由你写代码时将变量和块作用域写在哪里来决定的,词法分析器处理代码时会保持作用域不变。那么究竟什么时候才会生成新的作用域呢?最常见的答案是 JavaScript 具有基于函数的作用域,这意味着每声明一个函数都会为其自身创建一个作用域。
函数作用域的含义是指, 属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的, 能充分利用 JavaScript 变量可以根据需要改变值类型的“动态” 特性。 通常我们会先声明一个函数,然后再做具体的实现。但是我们可以反过来想,换一个角度来理解函数,这样会有助于更好地理解函数作用域。从所写的代码中挑出一个代码片段,然后使用函数包装它们。这样就可以将代码包裹在一个函数的作用域中,然后用这个作用域来“隐藏”它们。这符合“最小暴露原则”,将具体的内容私有化,是一种良好的软件设计。 “隐藏” 作用域中的变量和函数所带来的另一个好处, 是可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但用途却不一样, 无意间可能造成命名冲突。冲突会导致变量的值被意外覆盖。例如:
function foo() {
function bar(a) {
i = 3; // 修改 for 循环所属作用域中的 i
console.log( a + i );
}
for (var i=0; i<10; i++) {
bar( i * 2 ); //无限循环了!
}
}
foo();
所以,在任意代码片段外部添加包装函数, 可以将内部的变量和函数定义“隐藏” 起来, 外部作用域无法访问包装函数内部的任何内容。如例:
var a = 2;
function foo() {
var a = 3;
console.log( a ); // 3
}
foo();
console.log( a ); // 2
但是这样会带来新的问题,引入了额外的污染,必须声明一个具名函数 foo() ,这污染了所在的作用域(在示例中是全局作用域)。其次,必须显式地通过函数名(foo()) 调用这个函数才能运行其中的代码。
如果函数不需要函数名(或者至少函数名可以不污染所在作用域), 并且能够自动运行,这将会更加理想。JavaScript 提供了能够同时解决这两个问题的方案。需要注意的是这两种方案使用的都是函数表达式,而不是函数声明。函数声明和函数表达式最重要的区别是它们的名称标识符将会绑定在何处。
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
(function foo(){ .. })()。第一个 ( ) 将函数变成表达式, 第二个 ( ) 执行了这个函数。这种方式成为立即执行函数表达式(IIFE)。(function foo(){ .. }) 作为函数表达式意味着 foo 只能在 .. 所代表的位置中被访问, 外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。 值得一提的是,在 ES6 支持 let 和 const 之后,IIFE 立即执行函数表达式已经完成了它的历史使命,可以退休了。
函数表达式是可以匿名的,但是函数声明是不可以匿名的(JavaScript 标准不允许)。 所以立即执行表达式的示例可以改写为匿名函数表达式的方式:
var a = 2;
(function() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
匿名也就意味着没有名字,不会被外部调用,自然不会污染外部作用域。我们见到最多的匿名函数表达式应该是回调函数。
setTimeout( function() {
console.log( "I waited 1 second!" );
}, 1000 );
匿名函数表达式书写起来简单快捷,但是却忽略了对代码可读性、可理解性非常重要的函数名,一个好的函数名是可以自描述的。所以我们在使用匿名函数表达式时应该着重考虑代码的可读性、可理解性。
尽管函数作用域是最常见的作用域单元, 当然也是现行大多数 JavaScript 中最普遍的设计方法。虽然这样,但是函数作用域实现起来却不是最简洁的,甚至有点啰嗦。块作用域可以很好的解决这一点,实现维护起来更加优秀、 简洁的代码。 下面的代码你一定很熟悉:
for (var i=0; i<10; i++) {
console.log( i );
}
我们在 for 循环的内部声明定义了变量i,只是想在 for 循环内部使用变量i,但是实际上,变量i 已经被绑定到外部作用域(示例是全局作用域)。
for (var i=0; i<10; i++) {
console.log( i );
}
console.log( i ); //10
继续看一个更过分的代码:
var foo = true;
if (foo) {
var bar = foo * 2;
console.log( bar ); //2
}
我们的本意是,在 if 分支中声明 bar 变量,仅在分支中可以使用 bar 变量。但是结果却是,在使用 var 声明变量时,它写在哪里都是一样的,最终都将会属于外部作用域。
var foo = true;
if (foo) {
var bar = foo * 2;
console.log( bar ); //2
}
console.log( bar ); //2
是不是很崩溃,更让人崩溃的是,在块作用域之前,我们只能使用立即执行函数表达式来解决“变量外泄”的问题。感谢块作用域,解救了我们于水火之中。我在前文提到过,在 ES6 支持 let 和 const 之后,IIFE 立即执行函数表达式已经完成了它的历史使命,可以退休了。
let 关键字遵循块作用域,而不是默认的词法作用域。可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。换句话说, let 通过 {} 块即可创建新的作用域,无需创建新的函数来创建新的作用域。
使用 let 声明变量可以很简单的解决上面的问题,这也是 let 的优势所在:
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); //ReferenceError: i is not defined
var foo = true;
if (foo) {
let bar = foo * 2;
console.log( bar ); //2
}
console.log( bar ); //ReferenceError: bar is not defined
我们在作用域的文章中,提到过引擎在作用域中进行 RHS 查找时,ReferenceError 代表作用域判别失败,也就是无法找到需要的变量。这下再也不用担心“变量外泄”的问题了。 const 关键字也遵循块作用域,可以使用它声明块作用域常量。有关 let 和 const 关键字的具体内容,将会在下篇文章中介绍。
•《你不知道的JavaScript》•《深入理解JavaScript特性》