作用域包含了一系列的“气泡”,每一个都可以作为容器,其中包含了标识符(变量、函数)的定义。这些气泡互相嵌套并且整齐地排列成蜂窝型,排列的结构是在写代码时定义的
函数中的作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复 用(事实上在嵌套的作用域中也可以使用)
函数作用域的这种特性是从最小特权原则中引申出来的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计
因此函数的作用域有两个主要作用:
- 变量私有化。隐藏内部声明,外界无法访问
- 规避冲突。避免同名标识符之间的冲突
- 全局命名空间。通常第三方库对外暴露一个特殊变量(对象),这个对象呗用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象的属性
- 模块管理。使用模块管理器,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中
函数作用域
我们知道,在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。但这并不理想:
- 首先,声明一个具名函数本身就污染了所在作用域;
- 其次,必须手动调用这个函数
幸好,JavaScript 提供了能够同时解决这两个问题的方案,立即执行函数1
2
3
4(function foo(){ // <-- 添加这一行 var a = 3;
console.log( a ); // 3
})(); // <-- 以及这一行
console.log( a ); // 2
我们看看这两种方式到底有什么不同?
首先,包装函数的声明以 (function...
而不仅是以 function...
开始,这不起眼的细节却非常重要。这样函数会被当作函数表达式而不是一个标准的函数声明来处理。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。
(function foo(){ .. })
foo 被绑定在函数表达式自身的函数中而不是所在作用域中,作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域;function foo() {...}
中 foo 被绑定在所在作用域中,可以直接通过foo()
来调用它
区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位 置。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
匿名和具名
1 | setTimeout( function () { |
这个函数其实是匿名函数表达式。因为函数表达式可以是匿名的,函数声明则不可以省略函数名。
匿名函数多用于回调函数,很多库和工具也倾向鼓励使用这种风格代码。但也有几个缺点:
- 匿名函数在栈追踪中不会显示有意义的函数名,调试困难;
- 引用自身(回调)困难;
- 匿名函数省略了名字,使得代码难以理解;
行内函数表达式可以避免上述问题1
2
3setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
console.log( "I waited 1 second!" );
}, 1000 );
立即执行函数表达式
1 | // 具名 IIFE |
由于函数被包含在一对 ( ) 括号内部,因此成为了一个表达式,几年前社区给它规定了一个术语:IIFE
,代表立即执行函数表达式 (Immediately Invoked Function Expression)
- IIFE 的一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。这样在代码风格上更清晰
- 还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,这种模式在 UMD(Universal Module Definition)项目中被广泛使用
1 | var a = 2; |
块作用域
尽管函数作用域是最常见的作用域单元,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简洁的代码。
比如像for
循环或者if
条件语句中声明的变量,我们通常希望变量只在语句内部有效,避免污染全局变量。这就是块作用域的用处。变量的声明应该距离使用的地方越近越好,并最大限度地本地化
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。
JS 中的块作用域
在 JS 中有一些语句天生带块作用域的功能。
1. with 语句
with
关键字,它不仅是一个难于理解的结构,同时也是块作用域的一个例子(块作用域的一种形式),用 with 从对象中创建出的作用域仅在 with
声明中而非外部作用域中有效。
不被推荐,因为它会影响性能,且不易阅读(代码块内的代码特别多的情况,根本不知道这个是普通的变量还是某个对象的属性,还是某个对象的属性的属性的属性)。
2. try/catch 语句
JavaScript
的 ES3 规范中规定 try/catch
的 catch
分句会创建一个块作用域,其中声明的变量仅在 catch
内部有效
这里可能有问题,亲测(chrome)内部的定义的变量外部仍可访问。可参考这里 或 这里。这是只属于err参数用的伪块作用域
3. let 和 const 关键字
ES6 中 let
和 const
关键字可以将变量绑定到所在的任意作用域中(通常是 {...}
内部)换句话说,let
和 const
为其声明的变量隐式地附加在了所在的块作用域。但const
值是固定的 (常量)
当然,只要声明是有效的,我们也可以显式地使用 { .. }
括号来为 let
创建一个用于绑定的块。
- 显式地创建块使变量的附属关系变得更加清晰
- 显式的块更方便地移动而不会对外部产生影响
++变量提升++
使用let
进行的声明不会在块作用域中进行提升
++垃圾收集++
块作用域有利于垃圾回收1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了! (这里指的是下面let定义的`someReallyBigData`)
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
++for 循环++
一个 let 可以发挥优势的典型例子就是 for 循环。for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环 的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。1
2
3
4
5
6
7
8
9
10
11
12for (let i=0; i<10; i++) {
console.log( i );
}
// 相当于
{
let j;
for (j=0; j<10; j++) {
let i = j; // 每个迭代重新绑定!
console.log( i );
}
}
总结
1.普通函数和 IIFE 的区别?
其中一个很非常重要的区别在于,普通函数会污染其所在的作用于变量;而 IIFE
不会。
2.函数作用域和块作用域?
JS 从软件设计的最小特权原则中引申出来的函数作用域和块作用域(ES6)
- 函数作用域 — 属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)
- 块作用域 — 指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指
{ .. }
内部)
ES6 中定义了块作用域,我们可以用{...}
很轻松的实现一个块作用域,let
和const
关键字也可以将变量绑定到特定块作用域中。
- 使用块作用域可以使我们更好的组织我们的代码,使其利于迁移;
- 快用于也更利于垃圾回收;