我们已经看到,我们可以拿来一段代码并在它周围包装一个函数,而这实质上对外部作用域“隐藏”了这个函数内部作用域包含的任何变量或函数声明。
例如:
var a = 2;
function foo() { // <-- 插入这个
var a = 3;
console.log( a ); // 3
} // <-- 和这个
foo(); // <-- 还有这个
console.log( a ); // 2
虽然这种技术“可以工作”,但它不一定非常理想。它引入了几个问题。首先是我们不得不声明一个命名函数 foo()
,这意味着这个标识符名称 foo
本身就“污染”了外围作用域(在这个例子中是全局)。我们要不得不通过名称(foo()
)明确地调用这个函数来使被包装的代码真正运行。
如果这个函数不需要名称(或者,这个名称不污染外围作用域),而且如果这个函数能自动地被执行就更理想了。
幸运的是,JavaScript 给这两个问题提供了一个解决方法。
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
仅能在 ..
代表的作用域中被找到,而不是在外部作用域中。将名称 foo
隐藏在它自己内部意味着它不会没必要地污染外围作用域。
匿名与命名
你可能对函数表达式作为回调参数再熟悉不过了,比如:
setTimeout( function(){
console.log("I waited 1 second!");
}, 1000 );
这称为一个“匿名函数表达式”,因为 function()...
上没有名称标识符。函数表达式可以是匿名的,但是函数声明不能省略名称 —— 那将是不合法的JS程序。
匿名函数表达式可以快速和很容易地键入,而且许多库和工具往往鼓励使用这种代码惯用风格。然而,它们有几个缺点需要考虑:
在栈轨迹上匿名函数没有有用的名称可以表示,这可能会使得调试更加困难。
没有名称的情况下,如果这个函数需要为了递归等目的引用它自己,那么就需要很不幸地使用 被废弃的
arguments.callee
引用。另一个需要自引用的例子是,当一个事件处理器函数在被触发后想要把自己解除绑定。匿名函数省略的名称经常对提供更易读/易懂的代码很有帮助。一个描述性的名称可以帮助代码自解释。
内联函数表达式 很强大且很有用 —— 匿名和命名的问题并不会贬损这一点。给你的函数表达式提供一个名称就可以十分有效地解决这些缺陷,而且没有实际的坏处。最佳的方法是总是命名你的函数表达式:
setTimeout( function timeoutHandler(){ // <-- 看,我有一个名字!
console.log( "I waited 1 second!" );
}, 1000 );
立即调用函数表达式
var a = 2;
(function foo(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
得益于包装在一个 ()
中,我们有了一个作为表达式的函数,我们可以通过在末尾加入另一个 ()
来执行这个函数,就像 (function foo(){ .. })()
。第一个外围的 ( )
使这个函数变成表达式,而第二个 ()
执行这个函数。
这个模式是如此常见,以至于几年前开发者社区一致同意给它一个术语:IIFE,它表示“立即被调用的函数表达式”(Immediately Invoked Function Expression)。
当然,IIFE不一定需要一个名称 —— IIFE的最常见形式是使用一个匿名函数表达式。虽然少见一些,但与匿名函数表达式相比,命名的IIFE拥有前述所有的好处,所以它是一个可以采用的好方式。
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
传统的IIFE有一种稍稍变化的形式,一些人偏好这样:(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 = true; // 给其他的代码埋地雷!别这么干!
(function IIFE( undefined ){
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
IIFE 还有另一种变种,它将事情的顺序倒了过来,要被执行的函数在调用和传递给它的参数 之后 给出。这种模式被用于 UMD(Universal Module Definition —— 统一模块定义)项目。一些人发现它更干净和易懂一些,虽然有点儿繁冗。
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
参数传入。