1.1 函数中的作用域
无论标识符声明出现在作用域中的何处,这个标识符所代表的变量或函数都将附属于所处作用域的气泡。
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
1.2 隐藏内部实现
隐藏内部实现就是可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”它们。
最小授权或最小暴露原则是指,在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。
1.2.1 规避冲突
“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突。冲突会导致变量的值被意外覆盖。
举个🌰
1 | function foo() { |
全局命名空间
当程序加载第三方库时,这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中。
模块管理
这些工具并没有能够违反词法作用域规则的“神奇”功能。它们只是利用作用 域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域 中,这样可以有效规避掉所有的意外冲突。
1.3 函数作用域
举个🌰:
1 | var a=2; |
以上代码会导致的问题:1、必须声明一个具名函数 foo(),意味着 foo 这个名称本身“污染”了所在作用域;2、必须显式地通过函数名(foo())调用这个函数才能运行其 中的代码。
为了解决这两个问题,JavaScript 提供了相关方案
1 | (function foo(){ // <-- 添加这一行 |
(function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中 被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。
区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
1.3.1 匿名和具名
函数表达式可以是匿名的,而函数声明则不可以省略函数名。
函数表达式的缺点:
- 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
- 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee引用, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑 自身。
- 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让 代码不言自明。
PS: arguments.callee在严格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。当一个函数必须调用自身的时候, 避免使用 arguments.callee(),通过要么给函数表达式一个名字,要么使用一个函数声明。关于arguments.callee
1.3.2 立即执行函数表达式
IIFE,代表立即执行函数表达式 (Immediately Invoked Function Expression);比如 (functionfoo(){..})() 。第一个( )将函数变成表 达式,第二个( )执行了这个函数。另一种改进的形式是: (function(){ .. }())。这两种形式在功能上是一致的。
IIFE 的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。
举个🌰:
1 | var a=2; |
这个模式的另外一个应用场景是解决 undefined 标识符的默认值被错误覆盖导致的异常(虽然不常见)。
举个🌰:
1 | undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!(这样做覆盖掉原来的undefined) |
IIFE 还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在 IIFE 执行之后当作参数传递进去。
举个🌰:
1 | (function IIFE( def ) { |
ps: 自己的理解:实参只是声明函数,调用还是在IIFE函数里面才执行。
1.4 块作用域
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息 扩展为在块中隐藏信息。
1.4.1 with
with是块作用域的一 个例子(块作用域的一种形式),用 with 从对象中创建出的作用域仅在 with 声明中而非外 部作用域中有效。
1.4.2 try/catch
JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作
用域,其中声明的变量仅在 catch 内部有效。
但是当同一个作用域中的两个或多个 catch 分句 用同样的标识符名称声明错误变量时,很多静态检查工具还是会发出警告。 实际上这并不是重复定义,因为所有变量都被安全地限制在块作用域内部, 但是静态检查工具还是会很烦人地发出警告。为了避免这个不必要的警告,可以将 catch 的参数命名为 err1、 err2 等。
1.4.3 let
let关键字可以将变量绑定到所在的任意作用域中(通常是{..}内部)。换句话说, let 为其声明的变量隐式地了所在的块作用域。
举个🌰:
1 | var foo = true; |
为了使变量的附属关系变得更加清晰。显式的代码优于隐式或一些精巧但不清晰的代码。
举个🌰:
1 | var foo = true; |
只要声明是有效的,在声明中的任意位置都可以使用 { .. } 括号来为 let (ps: 用var不可以咯)创建一个用于绑定的块。
但是使用 let 进行的声明不会在块作用域中进行提升。
1.4.4 const
ES6 还引入了 const,同样可以用来创建块作用域变量,但其值是固定的 (常量)。之后任何试图修改值的操作都会引起错误。
小结
函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ .. }内部)。
有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开 发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。