立即执行函数表达式(IIFE)
译者:nzbin
也许你还没有注意到,我是一个对术语比较坚持的人。因此,在听到很多次比较流行却容易产生误导的 JavaScript 术语“自执行匿名函数”之后,最终我决定把我的想法写成一篇文章。
为了提供关于这一模式如何运作的透彻信息,我已经提出了我们应该如何称呼它的建议,继续向下看。当然,如果你想跳过开头,你可以只看“自执行函数表达式”这一节,但是我建议你看完整篇文章。
请明白这篇文章并非要表达“我是对的,你是错的”这一观点。我真正感兴趣的是帮助人们理解一些潜在的复杂概念,并且让人们意识到使用一致的和准确的术语是人们能够做到以方便理解的最简单的事情之一。
那么,这到底是怎么回事呢?
在 JavaScript 中,每一个函数在执行时都会产生一个新的执行环境。由于在函数中定义的变量和函数只能在内部访问而不能被外部访问。这一执行环境调用的函数提供了一个非常简单的方法来创建私有作用域。
// 因为返回的函数有权访问私有变量 `i` function makeCounter() { // `i` 只能在 `makeCounter`内被访问. var i = 0; return function() { console.log( ++i ); }; } // 注意 `counter` 和 `counter2` 都有私有的作用域 `i`. var counter = makeCounter(); counter(); // logs: 1 counter(); // logs: 2 var counter2 = makeCounter(); counter2(); // logs: 1 counter2(); // logs: 2 i; // ReferenceError: i 未定义 (只存在 makeCounter 内部)
很多情况下,你并不需要 makeWhatever
函数返回多个实例,可以用一个实例来做。在其他情况下,你甚至没有明确的返回值。
这件事的核心
现在,无论你用 function foo(){}
还是
var foo = function(){}
的方式定义函数,最终都会以一个函数的标识符结尾,你可以通过圆括号 ()
调用函数,像 foo()
。
// 像这样定义的函数可以在函数名后放置 () 来执行 // 比如 foo(), 因为 foo 只是函数表达式 `function() { /* code */ }`的引用 var foo = function(){ /* code */ } // ...是不是只在函数表达式之后放置 () 就能执行? function(){ /* code */ }(); // SyntaxError: Unexpected token (
如你所见,有一个报错。当解析器在全局范围内或在函数中遇到 function
关键字时,默认情况下,它会认为这是函数声明而不是函数表达式。如果你没有明确告诉解析器这是一个表达式,它会认为这是一个匿名的函数声明并抛出意外的语法错误,因为函数声明需要名称。
题外话:函数,括号,语法错误
有趣的是,如果你为一个函数指定了名称并且在立刻在其后边放置了括号,解析器也会抛出错误,但原因不同。虽然在表达式之后放置括号说明这是一个将被执行的函数,但在声明之后放置括号会与前面的语句分离,成为一个分组操作符(可以作为优先提升的方法)。
// 现在这个函数声明的语法是正确的,但还是有报错 // 表达式后面的括号是非法的, 因为分组运算符必须包含表达式 function foo(){ /* code */ }(); // SyntaxError: Unexpected token ) // 如果你在括号内放置了表达式, 没有错误抛出... // 但是函数也不会执行, 因为: function foo(){ /* code */ }( 1 ); // 它与一个函数声明后面放一个完全无关的表达式是一样的: function foo(){ /* code */ } ( 1 );
你可以阅读 Dmitry A. Soshnikov 的文章来了解更多关于这方面的知识,ECMA-262-3 in detail. Chapter 5. Functions。
立即执行函数表达式(IIFE)
幸运的是,固定的语法错误很简单。最普遍接受的方式告诉解析器这是一个被括号包裹的函数表达式。因为在 JavaScript 中,括号内不能包含函数声明,在这一点上,当解析器遇到 function
关键字,它会以函数表达式而不是函数声明去解析它。
// 以下的任何一种方式都可以立即执行函数表达式,利用函数的执行环境 // 创建私有作用域 (function(){ /* code */ }()); // Crockford 推荐这个 (function(){ /* code */ })(); // 这个同样运行正常 // 因为括号和强制运算符的目的就是区分函数表达式和函数声明 // 它们会在解析器解析表达式时被忽略(但是请看下面的“重要提示”) var i = function(){ return 10; }(); true && function(){ /* code */ }(); 0, function(){ /* code */ }(); // 如果你不关心函数返回值或者你的代码变得难以阅读 // 你可以在函数前面加一个一元运算符 !function(){ /* code */ }(); ~function(){ /* code */ }(); -function(){ /* code */ }(); +function(){ /* code */ }(); // 下面是另一种变体, from @kuvos // 我不确定使用 `new` 关键字是否有性能影响, 但是能够正常运行 // http://twitter.com/kuvos/status/18209252090847232 new function(){ /* code */ } new function(){ /* code */ }() // 只需要使用括号传递参数
关于括号的注意事项
在函数表达式外面添加括号可以解除困惑,但这一情况并不是必须的,因为解析器已经预定义了一个函数表达式。作为约定,再做任务时使用括号仍然是一个好方法。
这一括号通常意味着函数表达式会被立即执行,变量将包含函数的结果而不是函数本身。这也会解决一些麻烦,否则如果你写了一个很长的函数表达式,别人必须拉到最底部查看该函数有没有被立即执行。
根据经验来说,书写明确的代码不仅可以避免浏览器抛出语法错误,也可以避免其他开发者对你说“WTFError”(what the fuck error)!
闭包的存储状态
就像函数被函数名调用时参数会被传递一样,立即执行函数表达式时参数同样会被传递。因为在一个函数内部定义的函数可以访问外部函数的变量(这种关系被称为闭包)。一个立即执行函数表达式可以用于封锁函数值并且有效的存储状态。
如果你想了解更多关于闭包的知识,请浏览Closures explained with JavaScript。
// 以下程序的运行结果和你想象的并不一样, 因为 `i` 的值 // 不会被锁定。相反,当点击每个链接的时候 (循环已经 // 结束), 会显示元素的总数, 因为那才是 // 点击时 `i` 实际的值. var elems = document.getElementsByTagName( 'a' ); for ( var i = 0; i < elems.length; i++ ) { elems[ i ].addEventListener( 'click', function(e){ e.preventDefault(); alert( 'I am link #' + i ); }, 'false' ); } // 以下程序会按你想象的方式运行, 因为在 IIFE 中, `i` 的值 // 会作为 `lockedInIndex` 被锁定。 循环结束之后, // 尽管 `i` 的值是元素总数, 但是在 IIFE 中 // `lockedInIndex` 的值是函数表达式调用时传入的(`i`)的值 // 因此当点击链接时, 显示的值是正确的。 var elems = document.getElementsByTagName( 'a' ); for ( var i = 0; i < elems.length; i++ ) { (function( lockedInIndex ){ elems[ i ].addEventListener( 'click', function(e){ e.preventDefault(); alert( 'I am link #' + lockedInIndex ); }, 'false' ); })( i ); } // 你也许会这样使用 IIFE , 只是包含 (返回) // 点击处理函数, 并不是整个 `addEventListener` 声明 // 无论哪种方式,两个示例都使用 // IIFE, 我发现前面的例子更易读懂 var elems = document.getElementsByTagName( 'a' ); for ( var i = 0; i < elems.length; i++ ) { elems[ i ].addEventListener( 'click', (function( lockedInIndex ){ return function(e){ e.preventDefault(); alert( 'I am link #' + lockedInIndex ); }; })( i ), 'false' ); }
注意最后两个例子,虽然 lock
edInIndex
可以获得 i
的值,但是使用一个不同的名称标识符作为函数参数可以使复杂的概念易于解释。
立即执行函数表达式最好的一方面就是,因为这个匿名函数表达式被立即执行,没有标识符,所以闭包的使用不会污染当前作用域。
“自执行匿名函数”有错误吗?
你已经发现这一称呼被提到了多次,但也许并不清晰,我已经提议“立即执行函数表达式”这一术语,如果你喜欢缩写,也可以称呼“IIFE”。“iffy”的发音提醒了我,我很喜欢,让我们这样称呼它吧。
“立即执行函数表达式”是什么?它是一个被立即执行的函数表达式,就像这个名称会让你相信一样。
我希望看到 JavaScript 社区成员在他们的文章和报告中采用“立即执行函数表达式”这个术语。因为我觉得这个术语使得理解这一概念变得简单,而“自执行匿名函数”这一术语并不准确。
// 这是一个自执行函数。 这种函数会递归地 // 执行 (或调用) 自身: function foo() { foo(); } // 这是一个自执行匿名函数。因为它没有 // 标识符, 必须使用 `arguments.callee` 属性 (它 // 表示当前执行的函数) 来调用自身。 var foo = function() { arguments.callee(); }; // 这 *可能* 是一个自执行匿名函数, 但只有当 // `foo` 标识符实际引用它的时候。如果你把`foo` 换成 // 别的东西, 你可能会有一个 "用于自执行" 的匿名函数。 var foo = function() { foo(); }; // 有些人把这个称为 "自执行匿名函数" ,其实它并 // 不是自执行, 因为它没有调用自身。它只是 // 立即调用。 (function(){ /* code */ }()); // 给函数表达式添加一个标识符 (因此创建了一个命名 // 函数表达式) ,调试时会非常有用。一旦命名, // 函数不再是匿名的。 (function foo(){ /* code */ }()); // IIFE 也可以自执行, 尽管这并不是最 // 有用的方式。 (function(){ arguments.callee(); }()); (function foo(){ foo(); }()); // 最后需要注意的一点: 这在 BlackBerry 5 中会报错, 因为 // 在一个命名函数表达式中, 函数名是 undefined。很奇怪,对吧? (function foo(){ foo(); }());
希望这些示例能够说明“自执行”的术语容易被误解,因为并不是函数执行自身,虽然函数被执行了。同样“匿名”也不具体,因为“立即执行函数表达式”既可以匿名也可以命名。因为相比“executed”,我更喜欢“invoked”,一个简单的原因是因为 头韵。我认为“IIFE”听上去比“IEFE”更好。
以上就是我的看法。
有趣的是:因为 arguments.callee
在ECMAScript 5 strict mode 严格模式下已经过时,所以无法在 ES5 的严格模式下创建“自执行匿名函数”。
最后的题外话:模块化
既然提到了函数表达式,如果我不说一下模块化就是我的疏忽。你不熟悉JavaScript的模块化也没关系,我的第一个示例非常简单,只是最终返回的是一个对象而不是函数(通常作为单例模式运行,如以下示例)
// 创建一个立即执行的匿名函数表达式, 然后 // 将它的 *返回值* 赋给一个变量。这种方法无须再 // 创建一个 `makeWhatever` 函数的引用。 // // 如同上面 "关于括号的注意事项" 中提到的一样, 尽管括号在函数 // 表达式中不是必须添加的, 但是按照习惯还是应该添加括号, // 因为这可以更清晰的表示出赋值给一个变量的是 // 函数的 *结果* 而不是函数自身 var counter = (function(){ var i = 0; return { get: function(){ return i; }, set: function( val ){ i = val; }, increment: function() { return ++i; } }; }()); // `counter` 是一个有属性的对象, 它的属性都是方法 counter.get(); // 0 counter.set( 3 ); counter.increment(); // 4 counter.increment(); // 5 counter.i; // undefined (`i` 不是返回对象的属性) i; // ReferenceError: i 未定义 (它只存在于闭包内)
模块化方法不仅强大而且简单。你可以用更少的代码有效地命名方法和属性,用一种方式组织所有的代码模块,并且可以避免全局变量的污染以及创建私有作用域。
扩展阅读
- ECMA-262-3 in detail. Chapter 5. Functions. - Dmitry A. Soshnikov
- Functions and function scope - Mozilla Developer Network
- Named function expressions - Juriy “kangax” Zaytsev
- JavaScript Module Pattern: In-Depth - Ben Cherry
- Closures explained with JavaScript - Nick Morgan
感谢您的阅读,如果您对我的文章感兴趣,可以关注我的博客,我是叙帝利,下篇文章再见!
开发低代码平台的必备拖拽库 https://github.com/ng-dnd/ng-dnd
低代码平台必备轻量级 GUI 库 https://github.com/acrodata/gui
适用于 Angular 的 CodeMirror 6 组件 https://github.com/acrodata/code-editor
基于 Angular Material 的中后台管理框架 https://github.com/ng-matero/ng-matero
Angular Material Extensions 扩展组件库 https://github.com/ng-matero/extensions
Unslider 轮播图插件纯 JS 实现 https://github.com/nzbin/unsliderjs
仿 Windows 照片查看器插件 https://github.com/nzbin/photoviewer
仿 Windows 照片查看器插件 jQuery 版 https://github.com/nzbin/magnify
完美替代 jQuery 的模块化 DOM 库 https://github.com/nzbin/domq
简化类名的轻量级 CSS 框架 https://github.com/nzbin/snack
与任意 UI 框架搭配使用的通用辅助类 https://github.com/nzbin/snack-helper
单元素纯 CSS 加载动画 https://github.com/nzbin/three-dots
有趣的 jQuery 卡片抽奖插件 https://github.com/nzbin/CardShow
悬疑科幻电影推荐 https://github.com/nzbin/movie-gallery
锻炼记忆力的小程序 https://github.com/nzbin/memory-stake