ES 函数基础 —— IIFE

published

ES2015 中定义的块级作用域,使应用广泛的 IIFE 变得不再必要了。

什么是 IIFE?

经常能看到如下的函数调用方式:

(function () {
  console.log("test")
})()


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

社区对这种形式的函数表达式调用的称呼不尽相同,其中包括:

  • 自执行匿名函数(self-executing anonymous function)
  • 立即执行函数表达式(Immediately-Invoked Function Expression,简称 IIFE)

IIFE 的基本形式是这样的:

(function () {
  // code
})()

以上代码定义并立即调用了一个匿名函数。代码中有两个重要的括号:

  1. 包含函数定义的圆括号,表示它是一个函数表达式,而不是一个函数声明
  2. 末尾的括号,表示立即调用这个函数

为什么第一对括号要这么用? 默认情况下,解释器会把遇到的 function 关键字当作是函数声明语句来进行解释。由于()之间只能包含表达式,所以解释器会把其中的内容当作表达式而不是声明语句来执行。

() 并不是唯一能创建函数表达式的方式,还有:

// 如果本身就是 expression,那么根本不需要做任何处理
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

// 如果你不在乎返回值,可以这么做
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();

// 还有更奇葩的方式,但性能不好
// 来自 http://twitter.com/kuvos/status/18209252090847232
new function(){ /* code */ }
new function(){ /* code */ }()

IIFE 的用途

模拟块级作用域

众所周知,在 ES2015 之前,ES 并没有 C 或 Java 中的块级作用域,只有函数级作用域。

function outputNumbers(count) {
  for (var i=0; i < count; i++) {
    console.log(i)
  }

  console.log(i)  // 计数
}

这个函数创建了一个 for 循环,变量 i 初始值被设置为 0 。在 Java 等语言中,变量 i 只会在 for 循环的代码块中有定义,循环一旦结束,变量 i 就会被销毁。可是在 ES2015 之前,由于没有块级作用域,变量 i 是定义在 outputNumbers()Variable Object 中的,因此从它有定义开始,就可以在函数内部的任何地方访问它。这是缺少块级作用域而引发的问题。不过可以使用 IIFE 的私有作用域来模拟块级作用域:

function outputNumbers(count) {
  (function () {
    for (var i=0; i < count; i++) {
      console.log(i)
    }
  })()

  console.log(i)  // 错误
}

IIFE 中的任何变量,都会在执行结束时被销毁。因此,变量 i 只能在循环中使用,使用后就被销毁。另外由于这个 IIFE 是一个闭包,它能够访问外部作用域中的所有变量,包括变量 count。 这种用法经常用在全局作用域中的函数上,从而限制向全局作用域中添加过多的变量和函数。

应该尽量减少向全局作用域中添加变量和函数,因为过多的全局变量和全局函数很容易导致变量冲突。

通过使用 IIFE 模拟块级作用域,每个开发者都可以使用自己的变量,又不必担心搞乱全局作用域。比如:

(function () {
  var now = new Date()
  if (now.getMonth() === 0 && now.getDate() === 1) {
    console.log('Happy new year!')
  } else {
    console.log('Go to school!')
  }
})()

模拟单例模式

var counter = (function () {
  var i = 0
  return {
    get: function () {
      return i
    },
    set: function (value) {
      i = value
    },
    increment: function () {
      return ++i
    },
    show: function () {
      console.log(i)
    }
  }
})()

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

解决闭包中的变量冲突

var f = function() {
  var res = []
  for(var i = 0; i < 10; i++) {
    var func = function () {
      console.log(i)
    }
    res.push(func)
  }

  return res
}

// 会输出 10 个 10,而不是预期的 0 1 2 3 4 5 6 7 8 9
var res = f()
for(var i = 0; i < res.length; i++) {
  res[i]()
}

修改后:

var f = function() {
  var res = []
  for(var i = 0; i < 10; i++) {
    // 添加一个 IIFE
    (function (index) {
      var func = function () {
        console.log(index)
      }
      res.push(func)
    })(i)
  }

  return res
}

// 输出预期的 0 1 2 3 4 5 6 7 8 9
var res = f()
for(var i = 0; i < res.length; i++) {
  res[i]()
}