立即执行函数表达式(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' ); }
注意最后两个例子,虽然 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(); //
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 = ...
随机推荐
- webapi - 使用依赖注入
本篇将要和大家分享的是webapi中如何使用依赖注入,依赖注入这个东西在接口中常用,实际工作中也用的比较频繁,因此这里分享两种在api中依赖注入的方式Ninject和Unity:由于快过年这段时间打算 ...
- 如何创建Vim Dotfile?
Dotfile是电脑系统里的隐藏文件,它是专门给更高级的用户,如开发者.程序员或工程师使用的,让他们用来调整系统.如何创建Vim-Dotfile? 可以参考以下步骤: 1. 首先,你要检查一下.vim ...
- 获取Canvas当前坐标系矩阵
前言 在我的另一篇博文 Canvas坐标系转换 中,我们知道了所有的平移缩放旋转操作都会影响到画布坐标系.那在我们对画布进行了一系列操作之后,怎么再知道当前矩阵数据状态呢. 具体代码 首先请看下面的一 ...
- C++随笔:.NET CoreCLR之GC探索(1)
一直是.NET程序员,但是.NET的核心其实还是C++,所以我准备花 一点时间来研究CoreCLR和CoreFX.希望这个系列的文章能给大家带来 帮助. GC的代码有很多很多,而且结构层次对于一个初学 ...
- 【踩坑速记】开源日历控件,顺便全面解析开源库打包发布到Bintray/Jcenter全过程(新),让开源更简单~
一.写在前面 自使用android studio开始,就被它独特的依赖方式:compile 'com.android.support:appcompat-v7:25.0.1'所深深吸引,自从有了它,麻 ...
- Unity3D 5.3 新版AssetBundle使用方案及策略
1.概览 Unity3D 5.0版本之后的AssetBundle机制和之前的4.x版本已经发生了很大的变化,一些曾经常用的流程已经不再使用,甚至一些老的API已经被新的API所取代. 因此,本文的主要 ...
- unity 3d 解析 json
官网案例传送门 我这里不过是借花献佛,案例官网就有. using UnityEngine; using System.Collections; public class json : MonoBeha ...
- HTML5 程序设计 - 使用HTML5 Canvas API
请你跟着本篇示例代码实现每个示例,30分钟后,你会高喊:“HTML5 Canvas?!在哥面前,那都不是事儿!” 呵呵.不要被滚动条吓到,很多都是代码和图片.我没有分开写,不过上面给大家提供了目录,方 ...
- Oracle创建表空间
1.创建表空间 导出Oracle数据的指令:/orcl file=C:\jds.dmp owner=jds 导入Oracle数据的指令:imp zcl:/orcl file=C:\jds.dmp fu ...
- 分布式存储 CentOS6.5虚拟机环境搭建FastDFS-5.0.5集群
前言: 由于公司项目需要,最近开始学习一下分布式存储相关知识,确定使用FastDFS这个开源工具.利用周末的时间在虚拟机上搭建了分布式存储系统,在搭建过程中,发现网上的资料说的并不是很全, ...