9.1 Immediately-Invoked Function

2018-11-18  本文已影响0人  牧羊少年之奇幻之旅

英文原文链接: Immediately-Invoked Function Expression (IIFE)
转载: 立即执行函数表达式(IIFE)
参考文章: 立即执行函数表达式(IIFE)

可能你并没有注意到,我对术语有点敏感,所以,当我听到流行的但是还存在误解的术语“自执行匿名函数”[self-executing anonymous function](或“自我调用的匿名函数”[self-invoking anonymous function])之后,我终于决定将我的想法整理成一篇文章。

除了提供关于这种模式如何实际工作的一些非常全面的信息之外,更进一步的,我实际上已经就我们应该称之为什么提出了建议,另外,如果你想跳过这里,可以查看一些实际的立即调用函数表达式(Immediately-Invoked Function Expressions),但我建议阅读整篇文章。

请理解,本文不打算成为“我是对的,你错了”的一类东西,我真的有兴趣帮助人们理解潜在复杂的概念,并认为使用一致和准确的术语是人们可以做的最容易理解的事情之一。

那么,这究竟是什么呢?

在 JavaScript 中,每个函数在被调用时都会创建一个新的执行上下文,因为函数中定义的变量和函数只能在该上下文中访问,而不能在该上下文之外访问,当调用函数时,函数提供的上下文提供了一个非常简单的方法创建私有变量。

// makeCounter 函数返回的是一个新的函数,该函数对makeCounter里的局部变量i享有使用权
function makeCounter() {
  // i 只是 makeCounter 函数内的局部变量
  var i = 0;

  return function () {
    console.log(++i);
  }
}
// 注意:counter 和 counter2 是不同的实例,他们分别拥有自己范围里的 i 变量。

var counter = makeCounter();
counter(); // expected output: 1
counter(); // expected output: 2

var counter2 = makeCounter();
counter2(); // expected output: 1
counter2(); // expected output: 2

console.log(i); // ReferenceError: i is not defined, 它只是 makeCounter 函数内的局部变量

在许多情况下,你可能并不需要makeWhatever(译者注:原文是makeWhatever,但是上文未提到)这样的函数返回多次累加值,并且可以只调用一次得到一个单一的值,在其他一些情况里,你甚至不需要明确的知道返回值。

核心

现在,无论您是定义函数function foo(){}还是var foo = function(){},您最终得到的是一个函数的标识符,您可以在它后面加上 parens(括号,()),比如:foo();

//像下面这样定义的函数可以通过在函数名后加一对括号进行调用,像这样`foo()`,
//因为foo相对于函数表达式`function(){/* code */}`只是一个引用变量。
var foo = function () {
  /* code */
};

// 它是否能说明函数表达式本身可以被调用呢?,仅仅通过在它之后放置()来调用它吗?
function () { /* code */}(); // Uncaught SyntaxError: Unexpected token (

正如你所看到的,这里捕获了一个错误。当圆括号为了调用函数出现在函数后面时,无论在全局环境或者局部环境里遇到了这样的function关键字,默认的,它会将它当作是一个函数声明,而不是函数表达式, 如果您没有明确告诉解析器期望表达式,它会看到它认为是没有名称的函数声明,并抛出SyntaxError异常,因为函数声明需要一个名称。

另外:functions,parens,SyntaxErrors

有趣的是,如果你为一个函数指定一个名字并在它后面放一对圆括号,同样的也会抛出错误,但这次是因为另外一个原因。当圆括号放在一个函数表达式后面指明了这是一个被调用的函数,而圆括号放在一个声明后面便意味着完全的和前面的函数声明分开了,此时圆括号只是一个简单的代表一个括号(用来控制运算优先的括号)。

// 虽然这个函数声明现在在语法上是有效的,但它仍然是一个声明,而下面一组 parens 是无效的,
// 因为分组操作符需要包含一个表达式
function foo() { /* code */}(); // Uncaught SyntaxError: Unexpected token )

// 现在,如果你在 parens 中放置一个表达式,不会抛出异常...但是函数也不会执行,因为:
function foo() {/* code */}(1);

// 实际上与此相当,一个函数声明后跟一个完全不相关的表达式:
function foo() { /* code */ }

(1);

您可以在Dmitry A. Soshnikov的文章ECMA-262-3(第5章 函数)中详细了解这方面的内容。

立即调用函数表达式(IIFE [Immediately-Invoked Function Expression])

幸运的是,SyntaxError “修复“很简单,最流行的也最被接受的方法是将函数声明包裹在圆括号里来告诉语法分析器去表达一个函数表达式,因为在 JavaScript 中,圆括号不能包含声明语句。此时,当解析器遇到 function 关键字时,它知道将其解析为函数表达式而不是函数声明。

/**
 *
 * 任何消除函数表达式和函数声明之间歧义的方法,都可以创建出立即调用函数声明。
 *
 */
// 可以使用以下两种模式中的任何一种立即调用函数表达式,利用函数的执行上下文来创建私有变量。
(function () { /* code */}()); // Crockford 建议这一个
(function () { /* code */ })(); // 但是这个也同样有效
(() => { /* code */})(); // 使用 ES6 箭头函数,虽然括号只允许在外面

// 因为 parens 或强制操作符的目的是消除函数表达式和函数声明之间的歧义,
// 当解析器已经获得期望的表达式时,可以省略它们。但是请参考下面的"重要提示"
var i = function () { return 10; }();
true && function () { /* code */ }();
0, function () { /* code */ }();

// 如果你不关心返回值,或者让你的代码稍微难以阅读,你可以通过在你的函数前面带上一个一元操作符来存储字节
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
void function () { /* code */ }();

// 这是另一种变体,来自@kuvos--我不确定使用`new`关键字的性能影响(如果有的话),但它有效。
new function () { /* code */ };
new function () { /* code */ }(); // 如果传递参数,只需要 parens

将变量传入到立即调用的函数表达式作用域中的步骤如下:

(function (a, b) { /* code */ })("hello", "world");

一个以括号开头的 IIFE(Immediately-Invoked Function Expression 立即调用函数表达式)使得自动分号插入(automatic semicolon insertion ASI)会导致一些问题。该 IIFE 会被解析为对上一行的最后一个术语的调用,在某些可省略分号的情况中,分号放在括号前面,称为防御分号。例如:

a = b + c

;(function () {
  // code
})();
// 避免被解析为 c();
关于那些 parens 的重要说明

在一些情况下,当额外的带着歧义的括号围绕在函数表达式周围是没有必要的(因为这时候的括号已经将其作为一个表达式去表达),但当括号用于调用函数表达式时,这仍然是一个好主意。

这样的括号指明函数表达式将会被立即调用,并且变量将会储存函数的结果,而不是函数本身。当这是一个非常长的函数表达式时,这可以节约别人阅读你代码的时间,不用滚动到页面底部去看这个函数是否被调用。

根据经验,虽然编写明确的代码可能在技术上是必要的,以防止 JavaScript 解析器抛出 SyntaxError 异常,编写明确的代码也是非常必要的,以防止其他开发人员抛出 “WTFError” 异常!

用闭包保存状态

就像当函数被它们的命名标识符调用时可以传递参数一样,立即执行函数表达式也能传参数。由于在另一个函数中定义的任何函数都可以访问外部函数的传入参数和变量(这种关系称为闭包),利用这一点,我们能使用立即执行函数锁住变量保存状态。

如果您想了解更多关于闭包的知识,请阅读用JavaScript解释的闭包

// 这不会像你想象的那样工作,因为 `i` 的值永远不会被锁定,
// 相反每次点击时(在循环完成后),都会提示元素的总数,
// 因为这就是此时 `i` 的值。
var elems = document.getElementsByTagName("a");

for (var i = 0; i < elems.length; i++) {
  elems[i].addEventListener("click", function (e) {
    e.preventDefault();
    alert('I am link #' + i);
  }, 'false');
}

// 这是有效的,因为在 IIFE 中,`i` 的值被锁定为 `lockedInIndex`,
// 在循环完成执行后,即使 `i` 的值是元素的总数,在调用函数表达式时,
// 在 IIFE 内部,`lockedInIndex` 的值是传入函数表达式(`i`)的值,
// 所以当点击一个链接时,正确的值被弹出。
var elems = document.getElementsByTagName("a");

for (var i = 0; i < elems.length; i++) {
  (function (lockedInIndex) {
    elems[i].addEventListener("click", function (e) {
      e.preventDefault();
      alert('I am a link #' + lockedInIndex);
    }, 'false');
  })(i);
}

// 您也可以像这样使用 IIFE,仅包含(并返回)click 处理函数,而不是整个 `addEventListener` 赋值。
// 无论哪种方式,虽然两个示例使用 IIFE 锁定值,但我发现前面的示例更具可读性。
var elems = document.getElementsByTagName("a");

for (var i = 0; i < elems.length; i++) {
  elems[i].addEventListener("click", (function (lockInIndex) {
    return function (e) {
      e.preventDefault();
      alert('I am a link #' + lockInIndex);
    }
  })(i), 'false');
}

请注意,在最后两个实例中,lockedInIndex可以没有任何问题的访问i,但是作为函数的参数使用一个不同的命名标识符可以使概念更加容易的被解释。

立即调用函数表达式的一个最有利的副作用是,因为这个未命名的或匿名的函数表达式被立即调用,而不使用标识符,所以可以使用闭包而不污染当前范围。

自执行匿名函数(“Self-executing anonymous function”)有什么问题呢?

您已经见过它被多次提到,但如果不清楚,我建议使用术语“立即调用函数表达式”和“IIFE”(如果您喜欢首字母缩略词)。“iffy”的发音是我提出来的,我很喜欢这个发音,我们继续。

什么是立即调用函数表达式? 这是一个立即被调用的函数表达式。 就像这个名字会让你相信。

我希望看到JavaScript社区成员在他们的文章和演示中采用“立即调用函数表达式”和“IIFE”这两个术语,因为我觉得这样理解这个概念更容易一些,而且因为“自执行匿名函数”这个术语并不准确:

// 这是一个自我执行的函数。 这是一个递归执行(或调用)自身的函数:
function foo() { foo(); }

// 这是一个自我执行的匿名函数,因为它没有标识符,所以它必须使用 `arguments.callee` 属性,
// (它指定当前正在执行的函数) 来执行他自己。
var foo = function () { arguments.callee(); };

// 这也许算是一个自执行匿名函数,但是仅仅当`foo`标识符作为它的引用时,
// 如果你将它换成用`foo`来调用同样可行
var foo = function () { foo(); }

// 有些人称之为“自我执行的匿名函数”,即使它不是自动执行的,
// 因为它不会自行调用。 然而,它立即被调用。
(function () { /* code */}());

// 为函数表达式增加标识符(也就是说创造一个命名函数)对我们的调试会有很大帮助。
// 一旦命名,函数将不再匿名。
(function foo() { /* code */}());

// IIFE也可以自我执行,尽管这也许不是最有用的模式。
(function () { arguments.callee(); }());
(function foo() { foo(); }());

//最后要注意的一件事:这会导致BlackBerry 5出错,因为
//在命名函数表达式中,该名称未定义。太棒了,对吧?
(function foo(){ foo(); }());

希望这些例子清楚的表明,“自我执行” 这个术语有些误导,因为它并不是执行自己的函数,尽管函数已经被执行,另外,“匿名” 是不必要的,因为立即调用的函数表达式可以是匿名的或者是命名的。至于我更喜欢“invoked”而非“executed”,这是一个简单的头韵问题; 我认为“IIFE”看起来和听起来比“IEFE”好。

就是这样了。 这是我的想法。

有趣的事实:因为arguments.callee在 ECMAScript5 严格模式中被弃用,实际上在 ECMAScript5 严格模式下创建“自我执行的匿名函数”在技术上是不可能的。

最后一点:模块模式

在我调用函数表达式的时候,如果我没有提到模块模式,那我就会失职。如果您对JavaScript中的模块模式不熟悉,它与我的第一个示例类似,但返回的是Object而不是Function(并且通常以单例实现,如本例中所示)。

//创建一个立即调用的匿名函数表达式,并将其*返回值*赋给变量。这种方法“切断了
//名为`makeWhatever`函数引用的“middleman”。

// 正如上面“重要说明”中所解释的,尽管在这个函数表达式周围不需要使用 parens,
// 但仍然应该将它们作为约定使用,以帮助阐明变量被设置为函数的*result*,而不是函数本身。
var counter = (function () {
  var i = 0;

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

// `counter` 是一个具有属性的对象,在本例中正好属性全是方法。
counter.get(); // expected output: 0
counter.set(3);

counter.increment(); // expected output: 4
counter.increment(); // expected output: 5

counter.i; // undefined(`i`不是返回对象的属性)
i; // Uncaught ReferenceError: i is not defined(它只存在于闭包内)

模块模式方法不仅非常强大,而且非常简单。使用很少的代码,您可以有效地命名相关方法和属性的名称空间,以一种最小化全局范围污染和创建隐私的方式组织整个代码模块。

上一篇下一篇

猜你喜欢

热点阅读