0.函数作用域
函数作用域指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用。)
1.最小授权or最小暴露原则
在软件设计中,应该最小限度地暴露必要内容,而将其它内容都‘隐藏’起来,比如某个模块或对象的API设计。
2.规避冲突
2.1. 全局命名空间
变量冲突的一个典型例子存在于全局作用域中。
当程序中加载了多个第三方库时,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。
这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中。
2.2. 模块管理
另外一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中。
显而易见,这些工具并没有能够违反词法作用域规则的“神奇”功能。它们只是利用作用域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域中,这样可以有效规避掉所有的意外冲突。
立即执行函数表达式
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
由于函数被包含在一对( ) 括号内部,因此成为了一个表达式,通过在末尾加上另外一个( ) 可以立即执行这个函数,比如(function foo(){ … })()。第一个( ) 将函数变成表达式,第二个( ) 执行了这个函数。
这种模式很常见,几年前社区给它规定了一个术语:IIFE,代表立即执行函数表达式(Immediately Invoked Function Expression);
IIFE 的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
相较于传统的IIFE形式,很多人都更喜欢另一个改进的形式:(function(){…}())。仔细观察其中的区别。第一种形式中函数表达式被包含在()中,然后在后面用另一个()括号来调用。第二种形式中用来调用的()括号被移进了用来包装的()括号中。
这两种形式在功能上是一致的,用哪个完全凭个人喜好。
块作用域
尽管函数作用域是最常见的作用域单元,当然也是现行大多数JavaScript 中最普遍的设计方法,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简洁的代码。
除JavaScript 外的很多编程语言都支持块作用域,因此其他语言的开发者对于相关的思维方式会很熟悉,但是对于主要使用JavaScript 的开发者来说,这个概念会很陌生。
尽管你可能连一行带有块作用域风格的代码都没有写过,但对下面这种很常见的JavaScript代码一定很熟悉:
for (var i=0; i<10; i++) {
console.log( i );
}
我们在for 循环的头部直接定义了变量i,通常是因为只想在for 循环内部的上下文中使用i,而忽略了i 会被绑定在外部作用域(函数或全局)中的事实。
这就是块作用域的用处。变量的声明应该距离使用的地方越近越好,并最大限度地本地化。
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。
开发者需要检查自己的代码,以避免在作用范围外意外地使用(或复用)某
些变量,如果在错误的地方使用变量将导致未知变量的异常。变量i 的块作用域(如果存在的话)将使得其只能在for 循环内部使用,如果在函数中其他地方使用会导致错误。这对保证变量不会被混乱地复用及提升代码的可维护性都有很大帮助。
try/catch
非常少有人会注意到JavaScript 的ES3 规范中规定try/catch 的catch 分句会创建一个块作用域,其中声明的变量仅在catch 内部有效。
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found
尽管这个行为已经被标准化,并且被大部分的标准JavaScript 环境(除了老
版本的IE 浏览器)所支持,但是当同一个作用域中的两个或多个catch 分句用同样的标识符名称声明错误变量时,很多静态检查工具还是会发出警告。
实际上这并不是重复定义,因为所有变量都被安全地限制在块作用域内部,但是静态检查工具还是会很烦人地发出警告。
为了避免这个不必要的警告,很多开发者会将catch 的参数命名为err1、err2 等。也有开发者干脆关闭了静态检查工具对重复变量名的检查。
let
let 关键字可以将变量绑定到所在的任意作用域中(通常是{ … } 内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
用let 将变量附加在一个已经存在的块作用域上的行为是隐式的。
在开发和修改代码的过程中,如果没有密切关注哪些块作用域中有绑定的变量,并且习惯性地移动这些块或者将其包含在其他的块中,就会导致代码变得混乱。
为块作用域显式地创建块可以部分解决这个问题,使变量的附属关系变得更加清晰。通常来讲,显式的代码优于隐式或一些精巧但不清晰的代码。显式的块作用域风格非常容易书写,并且和其他语言中块作用域的工作原理一致
var foo = true;
if (foo) {
{ // <-- 显式的快
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
只要声明是有效的,在声明中的任意位置都可以使用{ … } 括号来为let 创建一个用于绑定的块。在这个例子中,我们在if 声明内部显式地创建了一个块,如果需要对其进行重构,整个块都可以被方便地移动而不会对外部if 声明的位置和语义产生任何影响。
使用let 进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。
(提升是指声明会被视为存在于其所出现的作用域的整个范围内。)
{
console.log( bar ); // ReferenceError!
let bar = 2;
}
let特性
1.垃圾收集
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
为变量显式声明块作用域,并对变量进行本地绑定是非常有用的工具,可以把它添加到你的代码工具箱中了。
2.let循环
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
for 循环头部的let 不仅将i 绑定到了for 循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
下面通过另一种方式来说明每次迭代时进行重新绑定的行为:
{
let j;
for (j=0; j<10; j++) {
let i = j; // 每个迭代重新绑定!
console.log( i );
}
}
const
除了let 以外,ES6 还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误。
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在if 中的块作用域常量
a = 3; // 正常!
b = 4; // 错误!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
小结
函数是JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。
但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ … } 内部)。
从ES3 开始,try/catch 结构在catch 分句中具有块作用域。
在ES6 中引入了let 关键字(var 关键字的表亲),用来在任意代码块中声明变量。if(…) { let a = 2; } 会声明一个劫持了if 的{ … } 块的变量,并且将变量添加到这个块中。
有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。