孔乙己自己知道不能和他们谈天,便只好向孩子说话。有一回对我说道,“你读过JavaScript高程么?”我略略点一点头。他说,“读过书,……我便考一考。自调用匿名函数,怎样写的?”我想,讨饭一样的人,也配考我么?便回过脸去,不再理会。孔乙己等了许久,很恳切的说道,“不能写罢?……我教给你,记着!这些写法应该记着。将来做架构师的时候,写代码要用。”我暗想我和架构师的等级还很远呢,而且我们架构师也从不写代码;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是两个括号加一个匿名函数么?”孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说,“对呀对呀!……自调用匿名函数有N样写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚用指蘸了酒,想在柜上写代码,见我毫不热心,便又叹了口气,显出极惋惜的样子。
自调用匿名函数(这里指立即执行的匿名函数)是很常见的创建命名空间方法,在这篇文章我整理一下关于自调用匿名函数的相关知识:
函数声明和函数表达式
想知道自调用匿名函数的N种写法,必须先了解函数声明和函数表达式的概念,声明函数的时候,除了new Function构造函数方式,下面两种是我们常用的方式的语法定义:
函数声明:
function FunctionName(FormalParameterList) { FunctionBody }
函数表达式:
function [FunctionName](FormalParameterList) { FunctionBody }
函数声明的典型格式:
function functionName(arg1, arg2, ...){
<!-- function body -->
}
函数表达式的典型格式:
var variable=function(arg1, arg2, ...){
<!-- function body -->
}
var variable=function functionName(arg1, arg2, ...){
<!-- function body -->
}
从语法的定义上看,这两者几乎是一模一样的(唯一的区别是函数表达式可以省略函数名称),那么就解释器而言,当遇到这个结构的语句时,判定为函数表达式还是函数声明呢?就javascript的语法而言,如果一条语句是以function关键字开始,那么这段会被判定为函数声明。而函数声明是不能被立即执行的,这无疑会导致语法的错误(SyntaxError),因此就必须有一个办法,使解析器可以将之识别为函数表达式。
函数声明立即执行报错:
function runNow(){return "sss"}() //Uncaught SyntaxError: Unexpected token )
解析器识别函数定义的条件是以function关键字开始 ,那么只要在function关键字的前面添加任何其他的元素,就会让一个函数声明语句变成了一个表达式。
所以,任何消除函数声明和函数表达式间歧义的方法,都可以被解析器正确识别。比如:
var i = function(){return 10}(); // undefined
1 && function(){return true}(); // true
1, function(){alert('areyouok')}(); // undefined
赋值,逻辑,甚至是逗号,各种操作符都可以告诉解析器,这个不是函数声明,它是个函数表达式。并且,对函数一元运算可以算的上是消除歧义最快的方式,感叹号只是其中之一,如果不在乎返回值,这些一元运算都是有效的:
!function(){alert('areyouok')}() // true
+function(){alert('areyouok')}() // NaN
-function(){alert('areyouok')}() // NaN
~function(){alert('areyouok')}() // -1
甚至下面这些关键字,都能很好的工作:
void function(){alert('areyouok')}() // undefined
new function(){alert('areyouok')}() // Object
delete function(){alert('areyouok')}() // true
最后,括号做的事情也是一样的,消除歧义才是它真正的工作,而不是把函数作为一个整体,所以无论括号括在声明上还是把整个函数都括在里面,都是合法的:
(function(){alert('areyouok')})() // undefined
(function(){alert('areyouok')}()) // undefined
既然有这么多种构建立即执行函数的方法,那他们的速度如何呢?在别的博客看到有人做了速度测试,结果如下:
根据上图中的数据可以看出:new方法永远最慢,其它方法很多差距其实不大,传统的括号,在测试里表现始终很快,在大多数情况下比感叹号更快,所以平时我们常用的方式毫无问题,甚至可以说是最优的。加减号在chrome表现惊人,而且在其他浏览器中速度也不错。
常用写法
格式
其实上文已经列举了十多种写法,那么我们平时该用哪一种写法呢,我们推荐
(function(){
//....
}());
为什么(function(){})()
不推荐呢,明明两种效果一模一样啊,那是为了预防一种少见的情况,别人拷贝了你的代码并放在他的代码下面,如果他的代码结尾处漏掉了分号,那就会报个让你一头雾水的错Uncaught TypeError: (intermediate value)(...) is not a function
,为了预防万一,可以在我们的代码前加个分号:
var foo=function(){
//别人的代码
}//注意这里没有用分号结尾
//开始我们的代码
;(function(){
//我们的代码。。
alert('Hello!');
})();
我们推荐的写法就不会出现这种问题哈哈,需要注意的是不要忘记结尾的分号:
(function() {
console.log('test');
}()) //上下文如此这般时,这里不加分号会报错
(function() {
console.log('test1');
}())
传参
下面介绍一下常用的系统变量 传参选择,我们知道,jQuery2.1.0中是这样做的:
(function(window, factory) {
factory(window)
}(typeof window !== "undefined" ? window : this, function() {
return function() {
//jQuery的调用
}
}))
为什么要把window作为参数传入呢?答案是当我们这样做之后,window在函数内部就有了一个局部的引用,使用window时不需要将作用域链退回到顶部作用域,可以提高访问速度,我们在开发jQuery插件时,常常选择将jQuery,window,document
传入插件内部以期获得更好的性能。
最后要传的参数就是undefined
,至于这个undefined,稍微有意思一点,在一些旧浏览器中可以修改undefined的值(这个行为在2009年的ECMAScript 5被修复了),为了得到没有被修改的undefined,我们并没有传递这个参数,但却在接收时接收了它,因为实际并没有传,所以undefined那个位置接收到的就是真实的undefined了:
(function(window, document,undefined) {
//...
})(window, document);
上述代码为最终格式。
小用法:用自执行表达式保存状态
利用自执行函数表达式锁住传入的参数,可以有效地保存状态。此处摘选自Ben Alman的博客: 原文出处 :
// 这个代码是错误的,因为变量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');
}
虽然本文中使用自调用匿名函数这一名称,但Ben Alman在博客中倡议使用立即调用的函数表达式(Immediately-Invoked Function Expression)这一名称,作者又举了一堆例子来解释,好吧,其实我是同意Ben Alman的叫法的,我们来看看:
// 这是一个自执行的函数,函数内部执行自身,递归
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里被废弃了,所以在这个模式下,其实是不能用的。
号外:函数的声明提前
既然前面提到了函数的声明和函数表达式,整好在这里介绍下两者的区别:大家都知道声明提前的说法, 声明提前是函数声明和函数表达式的一个重要区别,对于我们进一步理解这两种函数定义方法有着重要的意义。
var声明提前
但是再说函数声明提前之前呢,有必要说一下var
声明提前。先给出var
声明提前的结论:
变量在声明它们的脚本或函数中都是有定义的,变量声明语句会被提前到脚本或函数的顶部。但是,变量初始化的操作还是在原来var语句的位置执行,在声明语句之前变量的值是undefined。
上面的结论中可以总结出三个简单的点:
- 变量声明会提前到函数的顶部;
- 只是声明被提前,初始化不提前,初始化还在原来初始化的位置进行初始化;
- 在声明之前变量的值是undefined。
还是来例子实在:
var handsome='handsome';
function handsomeToUgly(){
alert(handsome);
var handsome='ugly';
alert(handsome);
}
handsomeToUgly();
正确的输出结果是:先输出undefined,然后输出ugly。
而不是:先输出handsome,然后输出ugly。
这里正是变量声明提前起到的作用。该handsome
局部变量在整个函数体内都是有定义的,在函数体内的handsome
变量覆盖了同名的handsome
全局变量,因为变量声明提前,即var handsome
被提前至函数的顶部,就是这个样子:
var handsome='handsome';
function handsomeToUgly(){
var handsome; //声明提前,覆盖了全局变量,使函数体内的handsome变为undefined
alert(handsome);
var handsome='ugly';
alert(handsome);
}
handsomeToUgly();
所以说在alert(handsome)
之前,已经有了var handsome
声明,由上面提到的
在声明之前变量的值是undefined
所以第一个输出undefined
。
又因为上面提到的:
只是声明被提前,初始化不提前,初始化还在原来初始化的位置进行初始化
所以第二个输出ugly
。
函数声明提前
接下俩我们结合var
声明提前开始聊函数声明的声明提前。
函数声明的声明提前小伙伴们应该很熟悉,举个再熟悉不过的例子。
sayTruth();<!-- 函数声明 -->
function sayTruth(){
alert('money is handsome.');
}
sayTruth();<!-- 函数表达式 -->
var sayTruth=function(){
alert('money is handsome.');
}
小伙伴们都知道,对于函数声明的函数定义方法,即上面的第一种函数调用方法是正确的,可以输出money is handsome.
的真理,因为函数调用语句可以放在函数声明之前。而对于函数表达式的函数定义方法,即上面的第二种函数调用的方法是不能输出money is handsome.
的正确结果的。
结合上面的money is handsome.
例子,函数声明提前的结论似乎很好理解,不就是在使用函数声明的函数定义方法的时候,函数调用可以放在任意位置嘛,这一点即为:
函数声明提前的时候,函数声明和函数体均提前了。
而且:
函数声明是在预执行期执行的,就是说函数声明是在浏览器准备执行代码的时候执行的。因为函数声明在预执行期被执行,所以到了执行期,函数声明就不再执行。
函数表达式为什么不能声明提前
我们再说一点:为什么函数表达式不能像函数声明那样进行函数声明提前呢?
我们上面说了var的声明提前,注意我上面提过的:
只是声明被提前,初始化不提前,初始化还在原来初始化的位置进行初始化
Ok,我们把函数表达式摆在这看看:
var variable=function(arg1, arg2, ...){
<!-- function body -->
}
函数表达式就是把函数定义的方式写成表达式的方式,就是把一个函数对象赋值给一个变量,所以我们把函数表达式写成这个样子:
var varible = 2333;
看到这,也许小伙伴们会明白了,一个是把一个值赋值给一个变量,一个是把函数对象赋值给一个变量,两者在声明时都遵循var
声明规则,所以对于函数表达式,变量赋值是不会提前的,即function(arg1, arg2, ...){}
是不会提前的,变量varible为undefined,所以函数表达式不能像函数声明那样进行函数声明提前。
函数声明提前的实例分析
还是那句话,还是例子来的实在:
sayTruth();
function sayTruth(){alert('handsome')};
function sayTruth(){alert('ugly')};
浏览器的输出结果是输出ugly
,因为函数声明提前,所以函数声明会在代码执行前进行解析,执行顺序是这样的,先解析function sayTruth(){alert('handsome')}
,在解析function sayTruth(){alert('ugly')}
,覆盖了前面的函数声明,当我们调用sayTruth()
函数的时候,也就是到了代码执行期间,声明会被忽略,所以自然会输出ugly
。忘了的可以看上面说过的:
函数声明是在预执行期执行的,就是说函数声明是在浏览器准备执行代码的时候执行的。因为函数声明在预执行期被执行,所以到了执行期,函数声明就不再执行了。
不被遵守的函数声明规范
ECMAScript规范中表示,函数声明语句可以出现在全局代码中,或者内嵌在其他函数中,但是不能出现在循环、条件判、或者try/finally以及with语句中。
的确是这样,但是规定下发了,遵守不遵守就是另一回事了。JavaScript对于这条规范的实现并不是严格遵守的。
以下的代码在Chrome和Firefox中的运行结果为:
sayTruth() //Uncaught TypeError: sayTruth is not a function
if(1) {
sayTruth() //m is handsome
function sayTruth() {
alert('m is handsome')
};
}
sayTruth() //m is handsome
号外部分摘自myvin 的函数声明和函数表达式——函数声明的声明提前
写在最后
中秋过后,秋风是一天凉比一天,看看将近初冬;我整天的靠着火,也须穿上棉袄了。一天的下半天,没有一个项目,我正合了眼坐着。忽然间听得一个声音,“兄弟买挂么。”这声音虽然极低,却很耳熟。看时又全没有人。站起来向外一望,那孔乙己便在服务器下对了门槛坐着。他脸上黑而且瘦,已经不成样子;穿一件破夹袄,盘着两腿,下面垫一个蒲包,用草绳在肩上挂住;见了我,又说道,“兄弟买挂么。”项目经理也伸出头去,一面说,“孔乙己么?你还有十九个bug没改呢!”孔乙己很颓唐的仰面答道,“这……下回再改罢。这一回卖的是大罗金仙挂,比斗圣挂要好。”项目经理仍然同平常一样,笑着对他说,“孔乙己,你又写了bug了!”但他这回却不十分分辩,单说了一句“不要取笑!”“取笑?要是不写bug,怎么会打断腿?”孔乙己低声说道,“跌断,跌,跌……”他的眼色,很像恳求项目经理,不要再提。此时已经聚集了几个人,便和架构师都笑了。我温了酒,端出去,放在门槛上。他从破衣袋里摸出四文大钱,放在我手里,见他满手是泥,原来他便用这手走来的。不一会,他喝完酒,便又在旁人的说笑声中,坐着用这手慢慢走去了。
至此,自调用匿名函数的知识点就交代的差不多了,本文完。
网友评论