IIFE-立即调用函数表达式

1,329 阅读4分钟

定义

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

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

// IIFE
(function () {
  console.log("I am IIFE");
})();

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

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

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

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

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

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

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

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

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

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

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

再补充一个奇怪的情况:

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

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

// 函数声明
function foo() {
  console.log("hello");
}

// 一个表达式
(1);

关于非匿名自执行函数

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

var b = 10;
(function b(){
	b = 20;
  console.log(b);
}())

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

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

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

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,可以用立即执行函数来模拟块级作用域,避免全局变量污染。

(function() {
  var foo = "bar";
  console.log(foo);
})();

foo; // ReferenceError: foo is not defined

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

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

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

}());

模块化封装

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

var myobj = (function () {
	// 私有成员
	var name = "my, oh my";

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

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

其他

经典题

下面这道经典题目

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**

blockLexicalEnvironment = {
  i: 0,
  outer: <globalLexicalEnvironment>
}
blockLexicalEnvironment = {
  i: 1,
  outer: <globalLexicalEnvironment>
}
blockLexicalEnvironment = {
  i: 2,
  outer: <globalLexicalEnvironment>
}
......

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

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

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

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

立即执行函数的递归

立即执行函数是如果不加名字,又想要自身递归调用怎么办呢?可以使用[arguments.callee](<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arguments/callee>),已在ES5严格模式中被废弃,了解原因可以看这里

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

const init = (function(n){
	if(n==1){
  	return 1;
  }else{
  	return n * arguments.callee(n-1);
  }
}(10))

console.log(init);    // 3628800

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

const init = (function a(n){
	if(n==1){
  	return 1;
  }else{
  	return n * a(n-1);
  }
}(10))

console.log(init);    // 3628800

关于变量提升

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

console.log(foo);       // 正常输出
console.log(sayName);   // Uncaught ReferenceError: sayName is not defined

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

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

参考

  1. Understanding Scope and Scope Chain in JavaScript
  2. Kyle Simpson. 2014. You Don't Know JS: Scope & Closures (1st. ed.). O'Reilly Media, Inc.