深入理解JavaScript系列(4) 立即调用的函数表达式


Posted in Javascript onJanuary 15, 2012

前言
大家学JavaScript的时候,经常遇到自执行匿名函数的代码,今天我们主要就来想想说一下自执行。
在详细了解这个之前,我们来谈了解一下“自执行”这个叫法,本文对这个功能的叫法也不一定完全对,主要是看个人如何理解,因为有的人说立即调用,有的人说自动执行,所以你完全可以按照你自己的理解来取一个名字,不过我听很多人都叫它为“自执行”,但作者后面说了很多,来说服大家称呼为“立即调用的函数表达式”。
本文英文原文地址:http://benalman.com/news/2010/11/immediately-invoked-function-expression/
什么是自执行?
在JavaScript里,任何function在执行的时候都会创建一个执行上下文,因为为function声明的变量和function有可能只在该function内部,这个上下文,在调用function的时候,提供了一种简单的方式来创建自由变量或私有子function。

// 由于该function里返回了另外一个function,其中这个function可以访问自由变量i 
// 所有说,这个内部的function实际上是有权限可以调用内部的对象。 
function makeCounter() { 
// 只能在makeCounter内部访问i 
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 
alert(i); // 引用错误:i没有defind(因为i是存在于makeCounter内部)。

很多情况下,我们不需要makeCounter多个实例,甚至某些case下,我们也不需要显示的返回值,OK,往下看。

问题的核心
当你声明类似function foo(){}或var foo = function(){}函数的时候,通过在后面加个括弧就可以实现自执行,例如foo(),看代码:

// 因为想下面第一个声明的function可以在后面加一个括弧()就可以自己执行了,比如foo(), 
// 因为foo仅仅是function() { /* code */ }这个表达式的一个引用 
var foo = function(){ /* code */ } 
// ...是不是意味着后面加个括弧都可以自动执行? 
function(){ /* code */ }(); // SyntaxError: Unexpected token ( 
//

上述代码,如果甚至运行,第2个代码会出错,因为在解析器解析全局的function或者function内部function关键字的时候,默认是认为function声明,而不是function表达式,如果你不显示告诉编译器,它默认会声明成一个缺少名字的function,并且抛出一个语法错误信息,因为function声明需要一个名字。
旁白:函数(function),括弧(paren),语法错误(SyntaxError)
有趣的是,即便你为上面那个错误的代码加上一个名字,他也会提示语法错误,只不过和上面的原因不一样。在一个表达式后面加上括号(),该表达式会立即执行,但是在一个语句后面加上括号(),是完全不一样的意思,他的只是分组操作符。
// 下面这个function在语法上是没问题的,但是依然只是一个语句 
// 加上括号()以后依然会报错,因为分组操作符需要包含表达式 
function foo(){ /* code */ }(); // SyntaxError: Unexpected token ) 
// 但是如果你在括弧()里传入一个表达式,将不会有异常抛出 
// 但是foo函数依然不会执行 
function foo(){ /* code */ }( 1 ); 
// 因为它完全等价于下面这个代码,一个function声明后面,又声明了一个毫无关系的表达式: 
function foo(){ /* code */ } 
( 1 );

你可以访问ECMA-262-3 in detail. Chapter 5. Functions 获取进一步的信息。
自执行函数表达式
要解决上述问题,非常简单,我们只需要用大括弧将代码的代码全部括住就行了,因为JavaScript里括弧()里面不能包含语句,所以在这一点上,解析器在解析function关键字的时候,会将相应的代码解析成function表达式,而不是function声明。
// 下面2个括弧()都会立即执行 
(function () { /* code */ } ()); // 推荐使用这个 
(function () { /* code */ })(); // 但是这个也是可以用的 
// 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的 
// 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了 
// 不过,请注意下一章节的内容解释 
var i = function () { return 10; } (); 
true && function () { /* code */ } (); 
0, function () { /* code */ } (); 
// 如果你不在意返回值,或者不怕难以阅读 
// 你甚至可以在function前面加一元操作符号 
!function () { /* code */ } (); 
~function () { /* code */ } (); 
-function () { /* code */ } (); 
+function () { /* code */ } (); 
// 还有一个情况,使用new关键字,也可以用,但我不确定它的效率 
// http://twitter.com/kuvos/status/18209252090847232 
new function () { /* code */ } 
new function () { /* code */ } () // 如果需要传递参数,只需要加上括弧()

上面所说的括弧是消除歧义的,其实压根就没必要,因为括弧本来内部本来期望的就是函数表达式,但是我们依然用它,主要是为了方便开发人员阅读,当你让这些已经自动执行的表达式赋值给一个变量的时候,我们看到开头有括弧(,很快就能明白,而不需要将代码拉到最后看看到底有没有加括弧。
用闭包保存状态
和普通function执行的时候传参数一样,自执行的函数表达式也可以这么传参,因为闭包直接可以引用传入的这些参数,利用这些被lock住的传入参数,自执行函数表达式可以有效地保存状态。
// 这个代码是错误的,因为变量i从来就没背locked住 
// 相反,当循环执行以后,我们在点击的时候i才获得数值 
// 因为这个时候i操真正获得值 
// 所以说无论点击那个连接,最终显示的都是I am link #10(如果有10个a元素的话) 
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'); 
} 
// 这个是可以用的,因为他在自执行函数表达式闭包内部 
// i的值作为locked的索引存在,在循环执行结束以后,尽管最后i的值变成了a元素总数(例如10) 
// 但闭包内部的lockedInIndex值是没有改变,因为他已经执行完毕了 
// 所以当点击连接的时候,结果是正确的 
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); 
} 
// 你也可以像下面这样应用,在处理函数那里使用自执行函数表达式 
// 而不是在addEventListener外部 
// 但是相对来说,上面的代码更具可读性 
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'); 
}

其实,上面2个例子里的lockedInIndex变量,也可以换成i,因为和外面的i不在一个作用于,所以不会出现问题,这也是匿名函数+闭包的威力。
自执行匿名函数和立即执行的函数表达式区别
在这篇帖子里,我们一直叫自执行函数,确切的说是自执行匿名函数(Self-executing anonymous function),但英文原文作者一直倡议使用立即调用的函数表达式(Immediately-Invoked Function Expression)这一名称,作者又举了一堆例子来解释,好吧,我们来看看:
// 这是一个自执行的函数,函数内部执行自身,递归 
function foo() { foo(); } 
// 这是一个自执行的匿名函数,因为没有标示名称 
// 必须使用arguments.callee属性来执行自己 
var foo = function () { arguments.callee(); }; 
// 这可能也是一个自执行的匿名函数,仅仅是foo标示名称引用它自身 
// 如果你将foo改变成其它的,你将得到一个used-to-self-execute匿名函数 
var foo = function () { foo(); }; 
// 有些人叫这个是自执行的匿名函数(即便它不是),因为它没有调用自身,它只是立即执行而已。 
(function () { /* code */ } ()); 
// 为函数表达式添加一个标示名称,可以方便Debug 
// 但一定命名了,这个函数就不再是匿名的了 
(function foo() { /* code */ } ()); 
// 立即调用的函数表达式(IIFE)也可以自执行,不过可能不常用罢了 
(function () { arguments.callee(); } ()); 
(function foo() { foo(); } ()); 
// 另外,下面的代码在黑莓5里执行会出错,因为在一个命名的函数表达式里,他的名称是undefined 
// 呵呵,奇怪 
(function foo() { foo(); } ());

希望这里的一些例子,可以让大家明白,什么叫自执行,什么叫立即调用。
注:arguments.callee在ECMAScript 5 strict mode里被废弃了,所以在这个模式下,其实是不能用的。

最后的旁白:Module模式
在讲到这个立即调用的函数表达式的时候,我又想起来了Module模式,如果你还不熟悉这个模式,我们先来看看代码:

// 创建一个立即调用的匿名函数表达式 
// return一个变量,其中这个变量里包含你要暴露的东西 
// 返回的这个变量将赋值给counter,而不是外面声明的function自身 
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; // 引用错误: i 没有定义(因为i只存在于闭包)

关于更多Module模式的介绍,请访问我的上一篇帖子:深入理解JavaScript系列(2):全面解析Module模式 。
更多阅读
希望上面的一些例子,能让你对立即调用的函数表达(也就是我们所说的自执行函数)有所了解,如果你想了解更多关于function和Module模式的信息,请继续访问下面列出的网站:
  1. ECMA-262-3 in detail. Chapter 5. Functions. - Dmitry A. Soshnikov
  2. Functions and function scope - Mozilla Developer Network
  3. Named function expressions - Juriy “kangax” Zaytsev
  4. 全面解析Module模式- Ben Cherry(大叔翻译整理)
  5. Closures explained with JavaScript - Nick Morgan
Javascript 相关文章推荐
XP折叠菜单&amp;仿QQ2006菜单
Dec 16 Javascript
jquery入门—数据删除与隔行变色以及图片预览
Jan 07 Javascript
JavaScript动态创建div属性和样式示例代码
Oct 09 Javascript
node.js中的fs.lchmodSync方法使用说明
Dec 16 Javascript
JS实现为表格动态添加标题的方法
Mar 31 Javascript
把Node.js程序加入服务实现随机启动
Jun 25 Javascript
js判断日期时间有效性的方法
Oct 24 Javascript
jQuery CSS3相结合实现时钟插件
Jan 08 Javascript
jQuery无刷新上传之uploadify简单代码
Jan 17 Javascript
javascript闭包功能与用法实例分析
Apr 06 Javascript
react中的ajax封装实例详解
Oct 17 Javascript
聊聊Vue中provide/inject的应用详解
Nov 10 Javascript
深入理解JavaScript系列(3) 全面解析Module模式
Jan 15 #Javascript
深入理解JavaScript系列(2) 揭秘命名函数表达式
Jan 15 #Javascript
深入理解JavaScript系列(1) 编写高质量JavaScript代码的基本要点
Jan 15 #Javascript
Prototype源码浅析 String部分(三)之HTML字符串处理
Jan 15 #Javascript
Prototype源码浅析 String部分(一)之有关indexOf优化
Jan 15 #Javascript
用js小类库获取浏览器的高度和宽度信息
Jan 15 #Javascript
javascript 文本框水印/占位符(watermark/placeholder)实现方法
Jan 15 #Javascript
You might like
PHP Curl出现403错误的解决办法
2014/05/29 PHP
ThinkPHP中__initialize()和类的构造函数__construct()用法分析
2014/11/29 PHP
PHP图片裁剪与缩放示例(无损裁剪图片)
2017/02/08 PHP
PHP函数按引用传递参数及函数可选参数用法示例
2018/06/04 PHP
php实现生成PDF文件的方法示例【基于FPDF类库】
2018/07/21 PHP
PHP htmlspecialchars() 函数实例代码及用法大全
2018/09/18 PHP
Avengerls vs KG BO3 第二场2.18
2021/03/10 DOTA
Jquery解析json字符串及json数组的方法
2015/05/29 Javascript
JS判断页面是否出现滚动条的方法
2015/07/17 Javascript
原生JS实现仿淘宝网左侧商品分类菜单效果代码
2015/09/10 Javascript
webstorm中配置nodejs环境及npm的实例
2018/05/15 NodeJs
Node.js assert断言原理与用法分析
2019/01/04 Javascript
vue-form表单验证是否为空值的实例详解
2019/10/29 Javascript
webpack中的模式(mode)使用详解
2020/02/20 Javascript
详解在Vue.js编写更好的v-for循环的6种技巧
2020/04/14 Javascript
JS轮播图的实现方法
2020/08/24 Javascript
简明 Python 基础学习教程
2007/02/08 Python
Python标准库之sqlite3使用实例
2014/11/25 Python
python概率计算器实例分析
2015/03/25 Python
浅析Python多线程下的变量问题
2015/04/28 Python
Python的Bottle框架中实现最基本的get和post的方法的教程
2015/04/30 Python
Python程序员面试题 你必须提前准备!(答案及解析)
2018/01/23 Python
Python格式化输出字符串方法小结【%与format】
2018/10/29 Python
Python判断telnet通不通的实例
2019/01/26 Python
Python实现简单的列表冒泡排序和反转列表操作示例
2019/07/10 Python
python3实现往mysql中插入datetime类型的数据
2020/03/02 Python
详细分析Python collections工具库
2020/07/16 Python
python实现图片转字符画
2021/02/19 Python
CSS3实现缺角矩形,折角矩形以及缺角边框
2019/12/20 HTML / CSS
BONIA官方网站:国际奢侈品牌和皮革专家
2016/11/27 全球购物
TUMI澳大利亚网站:美国旅行箱包品牌
2017/03/27 全球购物
国际知名军事风格休闲装品牌:Alpha Industries(阿尔法工业)
2017/05/24 全球购物
求职简历中自我评价
2014/01/28 职场文书
幼儿园门卫岗位职责
2014/02/14 职场文书
国际经济与贸易专业求职信
2014/07/10 职场文书
琅琊山导游词
2015/02/05 职场文书