函数立即表达式整理

本文主要介绍的是 Javascript 的立即执行函数的比较与总结, 列举了现在大部分的立即执行函数的写法,并且分析了错误的原因和正确的原因。


问题

来看几种写法,你能分辨出来,哪个是正确的,哪个是错误的吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 写法1
function a() {
console.log("a")
}();

// 写法2
(function b() {
console.log("b")
}());

// 写法3
(function c() {
console.log("c")
})();

// 写法4
var d = function d() {
console.log("d")
}
(d)()

// 写法5
var e = function e() {
console.log("e")
};
(e)()

// 写法6
!function f() {
console.log("f")
}();

// 写法7
+function g() {
console.log("g")
};

// 写法8
var h = function () {
console.log("h")
}();

// 写法9
(a === 1) && function () { /* ... */ } ();

// 写法 10
1, function () { /* ... */ }();

// 写法 11
function (){
console.log("a")
}();

// 写法 12
function j() {
console.log("j")
}(1)

上面的写法中,有几种是错误❌的,你找出来了吗?


解析

首先基本知识是,声明 javascript 的函数有两种方式,分别是语句(函数声明)的形式和表达式的形式,其中语句的形式是有变量提升的说法的,而表达式不会:

1
2
3
4
5
// 函数声明
function f() {}

// 表达式
var f = function f() {}

对于 Javscript 引擎而言, 如果 function 关键字出现在行首,会被解析成函数声明的形式。函数声明必须包括 : 关键词 function, 函数名,形参,函数体。

第一种(写法1)❌,因为直接开头出现 function 关键字, 有函数名,浏览器会认为这是函数声明,下面是函数的定义,但是它又是以圆括号结尾,所以就报错了。

实际上写法1等同于:

1
2
3
4
function a() {
// ...
}
()

这个空的()里面没有操作符,所以报了相应的错。
报错的内容是Uncaught SyntaxError: Unexpected token )

而第11种(写法11),浏览器也认为是函数声明的形式,但是却没有找到函数名,所以也是错误❌的。报错是:VM143:1 Uncaught SyntaxError: Unexpected token ( 。所以说这两种类似,但是报错的原因并不相同。

对于写法12, 实际上等同于

1
2
3
4
function j() {
console.log("j")
}
(1)

所以实际上,它不会报错,因为()有操作符,但是 j 函数也不会被执行, 最后返回的是 1, 因为只有 (1) 执行了。

那么,为什么2,3写法是正确✅的呢?因为当浏览器解析碰到了()时,()里面不能包含函数声明,所以浏览器会把这个解析成函数表达式。当它是函数表达式的时候,自然就立即执行了。并且也不会报错。

因此下面两种都是正确的。

1
2
(function(){ /* ... */ }());
(function(){ /* ... */ })();

所以如果我们要让它可以被浏览器识别,那么就让浏览器知道它是一个表达式吧。 因此6,7,9,10他们都是正确的。9,10这两种写法,前面一个是表达式,浏览器自然也认为后面一个也是表达式。

这种称为“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。 然而实际中,我们通常会叫立即执行函数。

但是注意立即调用的函数表达式后面一定要是有分号,如下:

1
2
3
4
!function () { /* ... */ }();
~function () { /* ... */ }();
-function () { /* ... */ }();
+function () { /* ... */ }();

如果没有分号,浏览器又会错误的识别了。认为第二行是第一行的参数。所以写法4正确,写法5错误❌, 这个一定要注意了。


作用

立即执行函数的作用,最常见的是配合闭包的使用,来保存函数局部状态。因为立即执行函数也能够传参数。所以有了最经典的:

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

这样可以分别打印出 i 从 1-5, 但是如果你像下面这样写,就会打印出5个5了。

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

更为经典的一个例子如下, 你想要对每个 a 元素都绑定一个不同的事件,这里 log 出 i 的值。但是却得不到你想要的结果,因为在我们执行点击事件的时候, i 已经被执行完,为5了。所以对于 a0, a1,a2,a3都没有被绑定相应的事件。

1
2
3
4
5
6
7
var elems = document.getElementsByTagName( 'a' );

for ( var i = 0; i < elems.length; i++ ) {
elems[i].addEventListener( 'click', function(e){
console.log( 'i,' + i );
}, 'false' );
}

我们用立即执行函数和闭包来改写下:

1
2
3
4
5
6
7
8
9
var elems = document.getElementsByTagName( 'a' );

for (var i = 0; i < eles.length; i++) {
(function (i) {
elems[i].addEventListener( 'click', function(e){
console.log( 'i,' + i );
}, 'false' );
})(i)
}

这样就 ok 了。注意 i 一定要在外部括号和里面的括号都传递进去,否则 i 不会被锁住。

立即执行函数当然还有别的好处,经常见的就是减少全局变量的定义,比如 jquery 的定义:

1
2
3
4
5
(function( $ ) {
$.fn.myPlugin = function() {
var a = 1;
};
})( jQuery );

这样减少了全局变量,每个插件自己用自己的变量,以防止冲突。性能也会好些。


总结

主要是以前其实这些知识点都是知道的,但是没有系统的总结或者思考过,这次算是有个稍微整体一点的认识。