首页 > 代码库 > 《你不知道的JavaScript》 函数作用域和块级作用域
《你不知道的JavaScript》 函数作用域和块级作用域
一、函数作用域
可用在代码外添加包装函数,将内部的变量和函数定义隐藏。
var a = 2;function foo() { // <- - 添加这一行 var a = 3; console.log( a ); //3} // <- - 以及这一行foo(); // <- - 以及这一行console.log( a ); //2
这种技术必须声明一个具名函数foo(),foo本身“污染”了所在作用域。其次,必须显式地通过函数名( foo( ) )调用这个函数才能运行其中的代码。
var a = 2;(function foo() { // <- - 添加这一行 var a = 3; console.log( a ); //3} )(); // <- - 以及这一行console.log( a ); //2
包装函数的声明以 (function 而不仅以function开始,函数会被当做 函数表达式而不是标准的函数声明。如果function是声明中的第一个词,则是一个函数声明,否则就是函数表达式。
函数声明和函数表达式最重要的区别是它们的名称标识符会绑定在哪。
第一个片段的foo被绑定在所在作用域,可以通过foo()来调用。第二个片段的foo被绑定在函数表达式自身的函数而不是所在的作用域。(function() {...})作为函数表达式意味着foo只能在...代表的位置中被访问,外部作用域不行。foo变量名被隐藏在自身意味着不会非必要地污染外部作用域。
二、匿名与具名
匿名函数有一些缺点:
- 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
- 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee,比如在递归。另一个函数需要引用自身的例子是在事件触发后事件监听器需要解绑自身。
- 匿名函数省略了对于代码可读性/可礼节性很重要的函数名。
可以给函数表达式指定函数名。
三、立即执行函数表达式(IIFE)
var a = 2;(function foo() { var a = 3; console.log( a );})();
由于函数被包含在一个括号内,因此成了一个表达式,通过在末尾加上()可以立即执行这个函数。第一个()将函数变成表达式,第二个()执行这个函数。
更多人使用改进形式(function() {...}())。它们的功能一样。
IIFE另一个非常普遍的进阶用法是把它们当做函数调用并传递参数进去。
var a = 2;(function IIFE( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2})( window );console.log( a ); // 2
我们将window对象的引用传递进去,但将参数命名为global,因此在代码风格上对全局对象的引用变得比引用一个没有“全局”字样的变量更清晰。
这个模式的另一个应用场景是解决undefined标识符的默认值被错误覆盖导致的异常。将一个参数命名为undefined,但在对应的位置不传入任何值,这样就可以保证在代码块中undefined标识符的值真的是undefined。
IIFE还有一种变化的用途是倒置代码的执行顺序,将需要运行的函数放在第二位,在IIFE执行后将参数传递进去。
var a = 2;(function IIFE( def ) { def(window);})(function def( global ) { var a = 3; console.log( a ); // 3 console.log( global.a); //2});
函数表达式def定义在片段的第二部分,然后当做参数(这个参数也叫def)被传递到IIFE函数定义的第一部分。最后参数def(传递进去的函数)被调用,并将window传入 当做global参数的值。
四、块作用域
在JavaScript中只有函数作用域,没有块级作用域。
for(var i = 0; i < 10; i++) {}console.log( i ); // 10
//在for循环的头部定义了变量i,通常只是想在for循环内部的上下文使用i,而忽略了i会被绑定到外部作用域(函数或全局)。
var foo = true;if (foo) { var bar = foo * 2; }console.log( bar ); // 2
//bar变量虽然在if声明中的上下文使用,但它们最终都属于外部作用域。
4.1 with
with也是块级作用域的一种形式,用with从对象中创建出的作用域仅在with声明中有效。
4.2 try/catch
ES3规范的try/catch的catch分句会创建一个块级作用域,其中声明的变量只在catch内部有效。
try { undefined();}catch(err) { var a = 0; console.log( err ); //可以执行}console.log( a ); // 0;console.log( err ); // err not found
4.3 let
ES6的let可以将变量绑定到所在的任意作用域(通常是{...})。
var foo = true;if (foo) { let bar = foo * 2; console.log( bar ); // 2}console.log( bar ); //referenceError
为块级作用域显式得创建块
var foo = true;if (foo) { { // < -- 显式的块 let bar = foo * 2; console.log( bar ); // 2 }}console.log( bar ); //referenceError
使用let进行的声明不会在块级作用域中提升。声明的代码被运行前,声明并不“存在”。
{ console.log( bar ); //ReferenceError let bar = 2;}
4.4 const
ES6引入const同样能创建块级作用域,但其值是常量。
var foo = true;if (foo) { var a = 2; const b = 3; // 包含在if中的块级作用域常量 a = 3; b = 4; // 错误}console.log( a ); // 3console.log( b ); // ReferenceError
《你不知道的JavaScript》 函数作用域和块级作用域