在JavaScript中,会遇到自执行匿名函数:
(function () {/*code*/} ) ()
。
这个结构大家并不陌生,但若要说:为什么要括弧起来?它的应用场景有哪些?……就会有点模糊。
此处作个小结。
本文篇幅比较长,但例子都很简单,可以跳跃式阅读。
一、函数的声明与执行
我们先来看下最初的函数声明与执行:
// 声明函数fun0 function fun0(){ console.log("fun0"); } //执行函数fun0 fun0(); // fun0
除了上面这种最常见的函数声明方式,还有变量赋值方式的,如下:
// 声明函数fun1 - 变量方式 var fun1 = function(){ console.log("fun1"); } // 执行函数fun1 fun1(); // fun1
二、 函数的一点猜想
既然函数名加上括号fun1()
就是执行函数。
思考:直接取赋值符号右侧的内容直接加个括号,是否也能执行?
试验如下,直接加上小括弧:
function(){ console.log("fun"); }();
以上会报错 line1:Uncaught SyntaxError: Unexpected token (
。
分析: function
是声明函数关键字,若非变量赋值方式声明函数,默认其后面需要跟上函数名的。
加上函数名看看:
function fun2(){ console.log("fun2"); }();
以上会报错 line3:Uncaught SyntaxError: Unexpected token )
。
分析: 声明函数的结构花括弧后面不能有其他符号(比如此处的小括弧)。
不死心的再胡乱试一下,给它加个实参(表达式):
function fun3(){ console.log("fun3"); }(1);
不会报错,但不会输出结果fun3
。
分析: 以上代码相当于在声明函数后,又声明了一个毫无关系的表达式。相当于如下代码形式:
function fun3(){ console.log("fun3"); } (1); // 若此处执行fun3函数,可以输出结果 fun3(); //"fun3"
三、自执行函数表达式
1. 正儿八经的自执行函数
想要解决上面问题,可以采用小括弧将要执行的代码包含住(方式一),如下:
// 方式一 (function fun4(){ console.log("fun4"); }()); // "fun4"
分析:因为在JavaScript语言中,()
里面不能包含语句(只能是表达式),所以解析器在解析到function
关键字的时候,会把它们当作function表达式,而不是正常的函数声明。
除了上面直接整个包含住,也可以只包含住函数体(方式二),如下:
// 方式二 (function fun5(){ console.log("fun5"); })();// "fun4"
写法上建议采用方式一(这是参考文的建议。但实际上,我个人觉得方式二比较常见)。
2. “歪瓜裂枣”的自执行函数
除了上面()
小括弧可以把function
关键字作为函数声明的含义转换成函数表达式外,JavaScript的&&
与操作、||
或操作、,
逗号等操作符也有这个效果。
true && function () { console.log("true &&") } (); // "true &&" false || function () { console.log("true ||") } (); // "true ||" 0, function () { console.log("0,") } (); // "0," // 此处要注意: &&, || 的短路效应。即: false && (表达式1) 是不会触发表达式1; // 同理,true || (表达式2) 不会触发表达式2
如果不在意返回值,也不在意代码的可读性,我们甚至还可以使用一元操作符(!
~
-
+
),函数同样也会立即执行。
!function () { console.log("!"); } (); //"!" ~function () { console.log("~"); } (); //"~" -function () { console.log("-"); } (); //"-" +function () { console.log("+"); } (); //"+"
甚至还可以使用new
关键字:
// 注意:采用new方式,可以不要再解释花括弧 `}` 后面加小括弧 `()` new function () { console.log("new"); } //"new" // 如果需要传递参数 new function (a) { console.log(a); } ("newwwwwwww"); //"newwwwwwww"
嗯,最好玩的是赋值符号=
同样也有此效用(例子中的i
变量方式):
//此处 要注意区分 i 和 j 不同之处。前者是函数自执行后返回值给 i ;后者是声明一个函数,函数名为 j 。 var i = function () { console.log("output i:"); return 10; } (); // "output i:" var j = function () { console.log("output j:"); return 99;} console.log(i); // 10 console.log(j); // ƒ () { console.log("output j:"); return 99;}
上面提及到,要注意区分 var i
和 var j
不同之处(前者是函数自执行后返回值给i
;后者是声明一个函数,函数名为j
)。如果是看代码,我们需要查看代码结尾是否有没有()
才能区分。一般为了方便开发人员阅读,我们会采用下面这种方式:
var i2 = (function () { console.log("output i2:"); return 10; } ()); // "output i2:" var i3 = (function () { console.log("output i3:"); return 10; }) (); // "output i3:" // 以上两种都可以,但依旧建议采用第一种 i2 的方式。(个人依旧喜欢第二种i3方式)
四、自执行函数的应用
1. for循环 + setTimeout 例子
直接来看一个例子。for
循环里面通过延时器输出索引 i
for( var i=0;i<3;i++){ setTimeout(function(){ console.log(i); } ,300); } // 输出结果 3,3,3
输出结果并不是我们所预想的0,1,2。当然,这个要涉及到setTimeout 的原理了,即使把300ms改成0ms,同样也会输出3,3,3
。具体可以查看博文 setTimeout(0) 的作用 。这里摘取其中一段说明。
JavaScript是单线程执行的,无法同时执行多段代码。当某段代码正在执行时,后续任务都必须等待,形成一个队列。只有当前任务执行完毕,才会从队列中取出下一个任务——也就是常说的“阻塞式执行”。
上面代码中设定了一个setTimeout
,那浏览器会在合适时间(此处是300ms后)把代码插入任务队列,等待当前的for
循环代码执行完毕再执行。(注意:setTimeout 虽然指定了延时的时间,但并不能保证执行的时间与设定的延时时间一直,是否准确取决于 JavaScript 线程是拥挤还是空闲。)
上面说了那么多,都是在分析为什么会输出3,3,3
。那怎么样才能输出1,2,3
呢?
看看下面的方式(写法一):把setTimeout
代码包含在匿名自执行函数里面,就可以实现“锁住”索引i
,正常输出索引值。
for( var i=0;i<3;i++){ (function(lockedIndex){ setTimeout(function(){ console.log(lockedIndex); } ,300); })(i); } // 输出 "0,1,2"
分析:尽管循环执行结束,i
值已经变成了3。但因遇到了自执行函数,当时的i
值已经被 lockedIndex
锁住了。也可以理解为 自执行函数属于for循环一部分,每次遍历i
,自执行函数也会立即执行。所以尽管有延时器,但依旧会保留住立即执行时的i
值。
上面的分析有点模糊和牵强,也可以从 闭包 角度出发分析的。但鄙人“闭包”概念模糊,先遗憾下,以后再补充分析了。QAQ
除了上面的写法,也可以直接在 setTimeout
第一个参数做自执行(写法二),如下。
注意: 写法二 会比 写法一 先执行。原因不明。
for( var i=0;i<3;i++){ setTimeout((function(lockedInIndex){ console.log(lockedInIndex); })(i) ,300); }
关于 自执行函数参数 lockedInIndex
,补充说明以下几点。
注意:自执行函数在 setTimeout 和在 setTimeout 里在第2、3中情况有区别(原因不明,后续再补)。
// 1. lockedInIndex变量,也可以换成i,因为和外面的i不在一个作用域 for( var i=0;i<3;i++){ (function(i){ setTimeout(function(){ console.log(i); // 1,2,3 } ,300); })(i); } for( var i=0;i<3;i++){ setTimeout((function(i){ console.log(i); // 1,2,3 })(i) ,300); } // 2. 自执行函数不带入参数 for( var i=0;i<3;i++){ (function(){ setTimeout(function(){ console.log(i); // 3,3,3 } ,300); })(); } for( var i=0;i<3;i++){ setTimeout((function(){ console.log(i); // 1,2,3 })() ,300); } // 3. 自执行函数只有实参没有写形参 for( var i=0;i<3;i++){ (function(){ setTimeout(function(){ console.log(i); // 3,3,3 } ,300); })(i); } for( var i=0;i<3;i++){ setTimeout((function(){ console.log(i); // 1,2,3 })(i) ,300); } // 4. 自执行函数只有形参没有写实参,这种情况不行。因为会导致输出 undefined。 for( var i=0;i<3;i++){ (function(i