立即执行函数表达式(IIFE)
原文:immediately-invoked-function-expression
译者: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' ); }
注意最后两个例子,虽然 lockedInIndex 可以获得 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(); //
counter.set( 3 );
counter.increment(); //
counter.increment(); // 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
立即执行函数表达式(IIFE)的更多相关文章
- javascript模块化编程-详解立即执行函数表达式IIFE
一.IIFE解释 全拼Imdiately Invoked Function Expression,立即执行的函数表达式. 像如下的代码所示,就是一个匿名立即执行函数: (function(windo ...
- 理解JavaScript的立即调用函数表达式(IIFE)
首先这是js的一种函数调用写法,叫立即执行函数表达式(IIFE,即immediately-invoked function expression).顾名思义IIFE可以让你的函数立即得到执行(废话). ...
- (译)详解javascript立即执行函数表达式(IIFE)
写在前面 这是一篇译文,原文:Immediately-Invoked Function Expression (IIFE) 原文是一篇很经典的讲解IIFE的文章,很适合收藏.本文虽然是译文,但是直译的 ...
- JS立即执行函数表达式(IIFE)
原文为 http://benalman.com/news/2010/11/immediately-invoked-function-expression/#iife ----------------- ...
- IIFE(立即执行函数表达式)
我们经常会看到这样的写法: ;(fuction () { // do something })() 这就是一个简单的IIFE(立即执行函数表达式,immediately-invoked functio ...
- 【JavaScript】浅析IIFE(立即执行函数表达式)的作用
什么是IIFE IIFE就是立即执行函数表达式(Immediately-Invoked Function Expression) 为什么需要IIFE 应用IIFE有两个比较经典的使用场景, 第一就是在 ...
- 详解javascript立即执行函数表达式(IIFE)
立即执行函数,就是在定义函数的时候直接执行,这里不是申明函数而是一个函数表达式 1.问题 在javascript中,每一个函数在被调用的时候都会创建一个执行上下文,在函数内部定义的变量和函数只能在该函 ...
- [转]Javascript中的自执行函数表达式
[转]Javascript中的自执行函数表达式 本文转载自:http://www.ghugo.com/javascript-auto-run-function/ 以下是正文: Posted on 20 ...
- IIFF(立即执行函数表达式)
立即执行函数表达式(Immediately-invoked function expression,IIFF) 在javascript(ES5)中,是没有块级作用域的概念的 for (var i = ...
随机推荐
- .NET Core & ASP.NET Core 1.0在Redhat峰会上正式发布
众所周知,Red Hat和微软正在努力使.NET Core成为Red Hat企业版Linux (RHEL)系统上的一流开发平台选项.这个团队已经一起工作好几个月了,RHEL对.NET有许多需求.今天在 ...
- 设计模式之行为类模式大PK
行为类模式大PK 行为类模式包括责任链模式.命令模式.解释器模式.迭代器模式.中介者模式.备忘录模式.观察者模式.状态模式.策略 ...
- 编写高质量代码:改善Java程序的151个建议(第8章:异常___建议114~117)
建议114:不要在构造函数中抛出异常 Java异常的机制有三种: Error类及其子类表示的是错误,它是不需要程序员处理也不能处理的异常,比如VirtualMachineError虚拟机错误,Thre ...
- WEB安全隐患
org.apache.commons.lang.StringEscapeUtils 进行输入框内容处理 [StringEscapeUtils.escapeSql(str);StringEscapeUt ...
- 自定义控件之 圆形 / 圆角 ImageView
一.问题在哪里? 问题来源于app开发中一个很常见的场景——用户头像要展示成圆的: 二.怎么搞? 机智的我,第一想法就是,切一张中间圆形透明.四周与底色相同.尺寸与头像相同的蒙板图片,盖在 ...
- 闭区间套定理(Nested intervals theorem)
① ②这里用到了极限与不等关系 ③如果a≠b,那么便不会有$\lim _{n\rightarrow \infty }\left| I_n \right| =0$ ④如果还存在一点c在内,那么同样也不会 ...
- Princeton Algorithms week3 Assignment
这周编程作业是实现检测点共线的算法.和排序算法有关系的地方在于,对斜率排序后可以很快的检测出来哪些点是共线的,另外这个算法的瓶颈也在于排序的性能. 一点收获: java传参数时传递的是值,这很多人都知 ...
- Android AppBar
AppBar官方文档摘记 2016-6-12 本文摘自Android官方文档,为方便自己及其他开发者朋友阅读. 章节目录为"Develop > Training > Best P ...
- ASP.NET Core 性能对比评测(ASP.NET,Python,Java,NodeJS)
前言 性能是我们日常生活中经常接触到的一个词语,更好的性能意味着能给我们带来更好的用户体检.比如我们在购买手机.显卡.CPU等的时候,可能会更加的关注于这样指标,所以本篇就来做一个性能评测. 性能也一 ...
- Java的几个同步辅助类
Java为我们提供了一些同步辅助类,利用这些辅助类我们可以在多线程编程中,灵活地把握线程的状态. CountDownLatch CountDownLatch一个同步辅助类,在完成一组正在其他线程中执行 ...