关于IIFE的那些前尘旧事

1,112 阅读8分钟

对于 IIFE(Immediately-Invoked-Function-Expression),即立即执行函数表达式,相信每个写过 JavaScript 的人都知道,但你真的理解 IIFE 吗?

为什么 IIFE 可以立即执行函数?IIFE 可以用来做哪些事情?本文带你了解 IIFE 的背后的故事。

0. 关于 IIFE 名字的由来

IIFE 这个名字来源于 2010 年 Ben Alman 的一篇 博客,在这篇博客中,他首次提出使用 Immediately-Invoked-Function-Expression 来命名我们现在熟悉的 IIFE。

而在此之前,IIFE 是被叫做 Self-Executing-Anonymous-Function(自我执行函数) 和 Self-Invoked-Anonymous-Function(自我唤醒函数)。

Ben Alman 认为这两种命名方式不够准确合理,因此提出 IIFE 的命名方式。

1. IIFE长啥样子

众所周知,IIFE 一般有两种形式:

( function () { /* do something */ } ) ()
( function () { /* do something */ } () ) 

这两种方式都可以使一个匿名函数立即执行。好了,你可以开始去用它了。

但是你有没有想过,为什么这样做可以使函数立即执行?

只有匿名函数可以吗?有名字的函数行不行?

有没有其他办法使函数立即执行?

要回答这些问题,先来看看函数是怎么被执行的。

2. 在 JavaScript 中函数是怎么被执行的?

首先来看看一个函数是怎么定义的,在 JavaScript 中,有两种定义函数的方法:

  1. 函数声明

    function hello() {
        console.log("hello world!")
    }
    
  2. 函数表达式

    const hello = function () {
        console.log("hello world!")
    }
    

然后怎么执行函数呢?在函数的名字后面加一个 () 就可以执行函数:

  hello() // "hello world!"

很稀松平常对不对?

在这里, hello 只是一个类似指针的变量,指向实际的函数对象, () 告诉 JavaScript 解释器执行 hello 指向的那个函数。

既然 () 的作用是用来执行函数,那么我们可不可以这样做来执行函数呢?

function hello() { console.log("hello world!") }()

或者这样:

function () { console.log("hello world!") }()

很遗憾并不能这样,解释器会报错。这是为什么呢?

第一段代码,浏览给会给出下面的错误信息:

image.png

这是因为解释器从左到右解释它遇到的代码,首先解释器遇到函数声明,没问题,创建这个函数。

接着执行 () 部分,但里面没有内容的 () 是没有任何语法意义的,所以浏览器会报错。

不信你可以在控制台中只输入 () 然后执行,浏览器会报告同样的错误:

image.png

对于第二段代码,浏览给会给出下面的错误信息:

image.png

这是因为解释器在执行代码时,首先遇到的是一个匿名函数的声明,而匿名函数的声明是必须被赋予给一个变量的,这里没有将匿名函数赋予给一个变量,所以出现语法错误,浏览器会抛出 Uncaugt SyntaxError: function statement requires a name 的错误提醒。

在这种情况下,后面的 () 根本就不会被执行。

那么有没有一种方法可以让函数在被定义的同时立即执行呢?

有的,这就是 IIFE,我们得让解释器把函数定义不看成函数声明,而是当做一个表达式,而表达式是可以被立即执行求值的,这样就可以在定义函数的同时立即执行函数。

3. () 操作符

那么怎么做才能让解释器把函数声明看做函数表达式呢?

最常见的答案是 () ,在JavaScript的语法中, () 叫做 Grouping Operator它里面只能接受表达式,可以用来改变表达式中计算的优先级的,放入 () 的内容会被自动转换成一个表达式,然后进行求值,举个例子:

a + b * c

上面这个表达式的计算顺序是:先计算 b * c ,再与 a 相加,计算图如下:

image.png

但是我们可以用 () 来改变计算优先级:

(a + b) * c

此时计算顺序就变成了先计算 a + b ,再与 c 相乘,计算图如下:

image.png

编程的本质是计算,而函数就是一系列计算操作的集合,可以这么说, () 操作符本意并不是用来执行函数的,它实质上是用来计算表达式的,只是函数看起来显得特殊一些,才约定把 () 放在函数的后面,用来表示这个函数正在执行:

a() + b() * c()

(a() + b()) * c()

4. IIFE的本质

因此我们将一个函数声明放在 () 里面,此时解释器就会将函数声明看做一个表达式,而表达式是可以被立即执行的,我们将一个普通的函数声明放入 () 中:

( function hello() { console.log("hello world!") } ) // no output

这里函数依旧没有执行,但是注意它的本质已经变了,解释器没有把它看做一个函数声明,而是看做一个表达式,这个表达式里只有一个操作,那就是声明一个函数,计算的结果是返回这个函数:

image.png

接下来我们在这个表达式后面加上 ()

( function hello() { console.log("hello world!") } () ) // "hello world!"

这个时候,这个表达式里有两个操作:(1) 声明一个函数 (2) 执行这个函数

image.png

或者我们也可以把 () 里面的 () 放在外面,这样 () 计算的结果是一个函数,然后再用 () 执行这个函数。

( function hello() { console.log("hello world!") } ) () // "hello world!"

image.png

上面这两段代码中使用的函数都是有名字的函数。没有名字的函数,即匿名函数也是一样的,将一个匿名函数声明放入 () 中:

( function () { console.log("hello world!") } )

解释器依旧会把这个匿名的函数声明当成表达式进行计算,结果是返回一个匿名函数:

image.png

如果再加上一个 () ,那么就变成了两步操作:创建一个匿名函数,然后执行这个匿名函数。

( function () { console.log("hello world!") }() ) // "hello world!"

同样地也可以把执行这个匿名函数的 () 放在外面,先执行 () 里面的表达式,这个表达式的计算结果是一个匿名函数,然后再用 () 执行这个匿名函数。

( function () { console.log("hello world!") } ) ()

这就是为什么IIFE可以立即执行的原因,将一个函数声明转换成,或者说放入一个表达式当中。

5. IIFE的其他形式

你以为事情到这里就算结束了?不,就算没有 () ,我们仍然可以使得一个函数在被声明的同时立即执行。

还记得 IIFE 的全称吗?Immediately-Invoked-Function-Expression,立即执行函数表达式,注意 IIFE 本质上是一个表达式

所以,只要将函数声明和函数的执行一起放入一个表达式中,这个函数就能立即执行,比如,在第一节中我们提到两种会报错的写法:

function hello() { console.log("hello world!") }() // Error

function () { console.log("hello world!") }() // Error

我们可以通过在其前面加上一个符合其运算规则的合法的运算符使其成为一个表达式:

+ function hello() { console.log("hello world!") }() // "hello world!" 
+ function () { console.log("hello world!") }() // "hello world!"

- function hello() { console.log("hello world!") }() // "hello world!" 
- function () { console.log("hello world!") }() // "hello world!"

! function hello() { console.log("hello world!") }() // "hello world!" 
! function () { console.log("hello world!") }() // "hello world!"

~ function hello() { console.log("hello world!") }() // "hello world!" 
~ function () { console.log("hello world!") }() // "hello world!"

也可以这样:

0, function () { console.log("hello world!") }() // "hello world!"

甚至这样:

true && function () { console.log("hello world!") }() // "hello world!"

总之,凡是将函数声明和执行放入一个合法的表达式里,都能使一个函数被声明的同时立即执行。

6. IIFE的使用场景

在解释清楚 IIFE 的原理后,我们来稍微讲讲 IIFE 的使用场景。

6.1 避免污染全局变量

为什么我们在使用 IIFE 的时候要使用匿名函数呢?因为习惯了,哈哈。

有的人可能会觉得因为 IIFE 的执行上还是在全局作用域里执行的,如果使用带名字的函数,那么这个函数就会挂载到全局作用域里,像下面这种情况,会觉得 hello 函数已经被挂载到全局作用域里了,产生了全局污染。

( function hello() { console.log("hello world!") } )() // "hello world"

其实并没有,无论是使用带名字的函数还是匿名函数,放入一个表达式中,只要这个表达式的计算结果不是一个函数,并且这个计算结果没有被赋予给一个变量,就不会产生全局污染。

依然拿上面这段代码举例,不过我们稍作一些改动:

( function hello() { console.log("hello world!") } )()
hello()

运行结果如图:

image.png

可以看到,hello 并没有挂载到 window 对象上,没有产生全局污染。

6.2 模块模式

在 CommonJS 和 AMD 没有出现之前,IIFE 很多时候都是用来模拟模块的,达到封装私有成员和向外提供特权函数的目的。

var counter = (function(){
	var i = 0;
	return {
		get: function(){
			return i;
		},
		set: function( val ){
			i = val;
		},
		increment: function() {
			return ++i;
		}
	};
}());

counter.get(); // 0
counter.set( 3 );
counter.increment(); // 4
counter.increment(); // 5

好啦,本篇文章至此就已经结束了,如果本文有任何遗漏或错误的地方,欢迎读者指出问题,我们一起学习进步!

7. 参考文献

  1. IIFE - MDN Web Docs Glossary: Definitions of Web-related terms | MDN

  2. Immediately-Invoked Function Expression (IIFE)

  3. Grouping operator ( ) - JavaScript | MDN