函数中的作用域
一般认为,JS具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个气泡,而其他结构都会创建作用域气泡。
但事实上并不完全正确。有闭包的存在。
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
隐藏内部实现
可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”它们。
为什么“隐藏”变量和函数是一个有用的技术?
由最小特权原则引申出来的,最小授权或最小暴露原则,这个原则指在软件设计中,应该最小限度暴露必要内容,而将其他内容“隐藏”起来。
规避冲突
“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突。
1. 全局命名空间
在全局作用域中声明一个名字足够独特的变量,通常为一个对象,这个对象作为命名空间,所有需要暴露给外界的功能都会变成这个对象(命名空间)的属性,而不是暴露自己在顶级的词法作用域中。
2. 模块管理
选择一个模块管理器使用
函数作用域
1 | var a = 2; |
通过添加包装函数来隐藏,会带来两个额外问题:
- 必须声明一个具名函数,具名函数的名称已经“污染”所在作用域;
- 必须显示的调用函数名才能执行函数
1 | var a = 2; |
包装函数声明以(function...
开始,而不是function...
。函数会被当作函数表达式而不是一个标准的函数声明来处理。
区分函数声明和函数表达式最简单的方法:
看function关键字出现在声明中的位置。如果function是声明中第一个词,就是一个函数声明,否则是一个函数表达式。
函数声明和函数表达式之间最重要的区别:
第一个片段中foo
被绑定在所在作用域,可以直接通过foo()
调用;
第二个片段中foo
被绑定在函数表达式自身的函数中而不是所在作用域。即,(function foo(){..})
作为函数表达式意味着foo
只能在..
说代表的位置中被访问,外部作用域不行。
匿名和具名
函数表达式可以说匿名,而函数声明不可以省略函数名。
匿名函数的几个缺点:
- 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难。
- 没有函数名,当函数引用自身时只能使用过期的
argument.callee
。 - 匿名函数使得函数的可读性/可理解性降低
始终给函数表达式命名是一个最佳实践:
1 | setTimeout( function timeoutHandle(){ |
立即执行函数表达式
IIFE(Immediately Invoked Function Expression)
虽然IIFE最常见的用法是使用匿名函数表达式,但使用具名函数的IIFE功能一样,且解决了匿名函数表达式的缺点,更值得推广。
(function(){..})()
和(function(){..}())
功能上一致,都可用。
IIFE另一个进阶用法,把他们当作函数调用并传递参数进去。
1 | var a = 2; |
块级作用域
块级作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。
表面上看,JS并没有块级作用域。
1. with
with
,是块级作用域的一种形式,用with
从对象中创建出的作用域仅在with
声明中而非外部作用域中有效。
2. try/catch
try/catch
中的catch分句会创建一个块级作用域,其中声明的变量仅在catch
内部有效。
1 | try { |
有多个catch分支时,参数命名为err1、err2、err3等
3. let
let
关键字可以将变量绑定到所在的任意作用域中(通常是{..}内部)。换句话说,let
为其声明的变量隐式的劫持了所在的块作用域。
1 | var foo = true; |
用let
将变量附加到一个已经存在的块级作用域上的行为是隐式,这会导致代码变得混乱。
为块作用域显示地创建块可以部分解决这个问题,使变量的附属关系变得更加清晰。
使用let
进行声明不会在块级作用域中进行提升。声明的代码被运行前,声明并不存在。
1 | { |
4. 垃圾收集
块作用域在碰到闭包存在情况下,有助于垃圾收集
5. let循环
1 | for (let i=0; i<10; i++) { |
for
循环头部的let
不仅将i绑定到for
循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
6. const
const
,同样创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误。
1 | var foo = true; |
小结
函数是JavaScript中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。
但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ .. }内部)。
从ES3开始,try/catch 结构在catch 分句中具有块作用域。
在ES6中引入了let
关键字(var 关键字的表亲),用来在任意代码块中声明变量。if(..){ let a = 2; }
会声明一个劫持了if 的{.. }
块的变量,并且将变量添加到这个块中。
有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。