定义

立即调用的匿名函数又被称作立即调用的函数表达式(IIFE, Immediately Invoked Function Expression)

它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// IIFE
(function () {
console.log("I am IIFE");
})();

// w3c 标准建议使用
(function () {
console.log("I am IIFE");
}());

// ES6 箭头函数写法
(() => {
console.log("I am IIFE");
})();

如果以function开头,则会被识别为函数声明,函数声明是不能被被执行符号()执行的!

1
2
3
4
5
6
7
8
function (){
console.log('a'); // Uncaught SyntaxError:
// Function statements require a function name
}(); // 这个报错是因为函数声明一定要有函数名

function foo(){
console.log('a'); // Uncaught SyntaxError: Unexpected token ')'
}();

只要是函数表达式,就可以加执行符号达到立即执行的效果了。

也可以加上一元操作符将函数转成表达式 (不推荐使用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var bar = function(){
console.log('Hello World');
}();

// 加上操作符,编译器不再认为这是一个函数声明

+function(){
console.log(1);// 1
}();
-function(){
console.log(2);// 2
}();
!function(){
console.log(3);// 3
}()
,function(){
console.log(4);// 4
}();

很多人习惯在前面加;,就是为了避免两个立即执行函数连在一起时发生如下错误:以下代码只能输出一个a

1
2
3
4
5
6
7
8
9
(function(){
console.log('a');
})()

(function(){
console.log('b');
})()

// Uncaught TypeError: (intermediate value)(...) is not a function

再补充一个奇怪的情况:

1
2
3
function foo(a) {
console.log("hello");
}(1);

这里不会报错,也不会输出,实际上这种写法会被解释成一个函数声明,还有一个无意义的表达式。也就是下面的样子:

1
2
3
4
5
6
7
// 函数声明
function foo() {
console.log("hello");
}

// 一个表达式
(1);

关于非匿名自执行函数

注意:立即执行函数也是可以加名字的,但是要注意,函数名只读。看下面这个例子

1
2
3
4
5
var b = 10;
(function b(){
b = 20;
console.log(b);
}())

在非严格模式下会输出[Function b],在严格模式下会报错!Uncaught TypeError: Assignment to constant variable.

原因就在于匿名函数属于表达式的范畴,如果添加了名字,遵从具名函数表达式的规范。

函数表达式中函数的识别名是不需要的,有名称的函数表达式,就是具名函数表达式 (Named function expressions, NFE),其函数的识别名,它的作用域是只在函数的主体内部

1
2
3
4
5
6
7
8
9
10
var b = 0;

var foo = function b(){
b = 10; // 严格模式下改行报错
console.log(b); // [Function b]
console.log(window.b); // 0
}

foo();
console.log(b); // 0

应用

进行初始化

ES6letconst,可以用立即执行函数来模拟块级作用域,避免全局变量污染。

1
2
3
4
5
6
(function() {
var foo = "bar";
console.log(foo);
})();

foo; // ReferenceError: foo is not defined

类似的,有一些操作需要在页面加载完成立即执行,比如绑定事件、创建对象等,也需要一些临时的变量,但是之后不会再用到。这时候使用立即执行函数,将这些初始化代码包裹在其局部作用域中,就是个很好的方案。

下面这个例子在初始化时绑定监听事件,count变量不会被泄漏出去,而且点击事件也能正常运作。

1
2
3
4
5
6
7
8
(function() {
var count = 0;

document.body.addEventListener('click', function() {
console.log("hi", count++);
});

}());

模块化封装

使用一个立即执行函数创建的闭包,实现对象字面量创建对象的私有成员。以此来封装模块,暴露的接口成为公有方法以供调用,私有成员外部无法取得。

1
2
3
4
5
6
7
8
9
10
11
12
13
var myobj = (function () {
// 私有成员
var name = "my, oh my";

// 实现公有部分
return {
getName: function () {
return name;
}
};
}());

myobj.getName(); // "my, oh my"

其他

经典题

下面这道经典题目

1
2
3
4
5
for(var i = 0; i < 5; i++ ) {
setTimeout(function(){
console.log(i);
},1000)
}

网上很多解释是说是因为事件队列的原因,说因为回调函数被放到事件队列中,for循环执行完毕i的值已经变成了5,再执行5console.log(i)所以会输出55

开始我看到这种解释有些困惑,为什么换成let就能解决呢?如果说是因为事件循环的原因,那换成let,循环完毕i也变成5了。

之后我补充了作用域,作用域链,执行上下文等知识,才了解到这种现象是由于作用域造成的。

之所以使用let就可以得到期望结果,是由于let的块级作用域,每一轮循环都会有一个新的词法作作用域环境保存每一轮的i

1
2
3
4
5
6
7
8
9
10
11
12
13
blockLexicalEnvironment = {
i: 0,
outer: <globalLexicalEnvironment>
}
blockLexicalEnvironment = {
i: 1,
outer: <globalLexicalEnvironment>
}
blockLexicalEnvironment = {
i: 2,
outer: <globalLexicalEnvironment>
}
......

之后执行console.log(i),由于当前作用域没有i,所以沿着作用域链找,即找到了他上一层的块级词法环境中的i

所以使用这里使用立即执行函数也是同样的原理。

1
2
3
4
5
6
7
for(var i = 0; i < 5; i++ ) {
(function(j){
setTimeout(function(){
console.log(j);
},1000);
}(i))
}

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的
作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

立即执行函数的递归

立即执行函数是如果不加名字,又想要自身递归调用怎么办呢?可以使用 arguments.callee,已在ES5严格模式中被废弃,了解原因可以看这里

calleearguments对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。

1
2
3
4
5
6
7
8
9
const init = (function(n){
if(n==1){
return 1;
}else{
return n * arguments.callee(n-1);
}
}(10))

console.log(init); // 3628800

当然,可以使用具名函数直接调用,如下:

1
2
3
4
5
6
7
8
9
const init = (function a(n){
if(n==1){
return 1;
}else{
return n * a(n-1);
}
}(10))

console.log(init); // 3628800

关于变量提升

匿名函数属于函数表达式,创建执行上下文时不会被提升。

1
2
3
4
5
6
7
8
9
10
console.log(foo);       // 正常输出
console.log(sayName); // Uncaught ReferenceError: sayName is not defined

(function sayName(name) {
console.log(name)
})('Millzie')

function foo() {
console.log("foo");
}

参考

https://blog.bitsrc.io/understanding-scope-and-scope-chain-in-javascript-f6637978cf53

https://stackoverflow.com/questions/103598/why-was-the-arguments-callee-caller-property-deprecated-in-javascript

Kyle Simpson. 2014. You Don’t Know JS: Scope & Closures (1st. ed.). O’Reilly Media, Inc.