《你不知道的JavaScript-上》函数作用域和块级作用域

函数中的作用域

一般认为,JS具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个气泡,而其他结构都会创建作用域气泡。
但事实上并不完全正确。有闭包的存在。

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

隐藏内部实现

可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”它们。

为什么“隐藏”变量和函数是一个有用的技术?
由最小特权原则引申出来的,最小授权或最小暴露原则,这个原则指在软件设计中,应该最小限度暴露必要内容,而将其他内容“隐藏”起来。

规避冲突

“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突。

1. 全局命名空间

在全局作用域中声明一个名字足够独特的变量,通常为一个对象,这个对象作为命名空间,所有需要暴露给外界的功能都会变成这个对象(命名空间)的属性,而不是暴露自己在顶级的词法作用域中。

2. 模块管理

选择一个模块管理器使用

函数作用域

1
2
3
4
5
6
7
var a = 2;
function foo() { // <-- 添加这一行
var a = 3;
console.log( a ); // 3
} // <-- 以及这一行
foo(); // <-- 以及这一行
console.log( a ); // 2

通过添加包装函数来隐藏,会带来两个额外问题:

  1. 必须声明一个具名函数,具名函数的名称已经“污染”所在作用域;
  2. 必须显示的调用函数名才能执行函数
1
2
3
4
5
6
var a = 2;
(function foo(){ // <-- 添加这一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及这一行
console.log( a ); // 2

包装函数声明以(function...开始,而不是function...。函数会被当作函数表达式而不是一个标准的函数声明来处理。

区分函数声明和函数表达式最简单的方法:
看function关键字出现在声明中的位置。如果function是声明中第一个词,就是一个函数声明,否则是一个函数表达式。

函数声明和函数表达式之间最重要的区别:
第一个片段中foo被绑定在所在作用域,可以直接通过foo()调用;
第二个片段中foo被绑定在函数表达式自身的函数中而不是所在作用域。即,(function foo(){..})作为函数表达式意味着foo只能在..说代表的位置中被访问,外部作用域不行。

匿名和具名

函数表达式可以说匿名,而函数声明不可以省略函数名。

匿名函数的几个缺点:

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难。
  2. 没有函数名,当函数引用自身时只能使用过期的argument.callee
  3. 匿名函数使得函数的可读性/可理解性降低

始终给函数表达式命名是一个最佳实践:

1
2
3
setTimeout( function timeoutHandle(){
console.log("libo")
},1000);

立即执行函数表达式

IIFE(Immediately Invoked Function Expression)
虽然IIFE最常见的用法是使用匿名函数表达式,但使用具名函数的IIFE功能一样,且解决了匿名函数表达式的缺点,更值得推广。

(function(){..})()(function(){..}()) 功能上一致,都可用。

IIFE另一个进阶用法,把他们当作函数调用并传递参数进去。

1
2
3
4
5
6
7
8
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );

console.log( a ); // 2

块级作用域

块级作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。
表面上看,JS并没有块级作用域。

1. with

with,是块级作用域的一种形式,用with从对象中创建出的作用域仅在with声明中而非外部作用域中有效。

2. try/catch

try/catch中的catch分句会创建一个块级作用域,其中声明的变量仅在catch内部有效。

1
2
3
4
5
6
7
8
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}

console.log( err ); // ReferenceError: err not found

有多个catch分支时,参数命名为err1、err2、err3等

3. let

let关键字可以将变量绑定到所在的任意作用域中(通常是{..}内部)。换句话说,let为其声明的变量隐式的劫持了所在的块作用域。

1
2
3
4
5
6
7
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError

let将变量附加到一个已经存在的块级作用域上的行为是隐式,这会导致代码变得混乱。
为块作用域显示地创建块可以部分解决这个问题,使变量的附属关系变得更加清晰。

使用let进行声明不会在块级作用域中进行提升。声明的代码被运行前,声明并不存在。

1
2
3
4
{
console.log(bar); //RerenceError
let bar=2;
}
4. 垃圾收集

块作用域在碰到闭包存在情况下,有助于垃圾收集

5. let循环
1
2
3
4
5
for (let i=0; i<10; i++) {
console.log( i );
}

console.log( i ); // ReferenceError

for循环头部的let不仅将i绑定到for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。

6. const

const,同样创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误。

1
2
3
4
5
6
7
8
9
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 的{.. }块的变量,并且将变量添加到这个块中。

有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。

liborn wechat
欢迎您扫一扫上面的微信二维码,订阅我的公众号!