闭包与即时函数

有一个重要的构造是在 JavaScript 高级函数式编程中经常使用的,这种构造依赖于对闭包的充分利用,比如:(function(){})()
这种模式的代码,毫无疑问可能用在很多地方,它给 JavaScript 语言带来了出乎意料的能力。让我们分解这段代码,探究其内部到底发生了什么。

首先,先忽略第一组括号的内容,再回头来看代码:(...)()

众所周知,可以通过函数名加圆括号:functionName(),的语法方式调用任意一个函数。但是在这里,我们可以使用任意一个引用函数实例的表达式作为函数的名称。就像下面的代码,使用变量名调用该变量所引用的函数:

与其它表达式在一起使用,我们需要将一个操作符——函数调用操作符 ( ) ,应用在整个表达式上,所以需要用圆括号将该表达式括起来。

也就是说,在 (...)() 中,第一组圆括号仅仅是用于划定表达式的范围,而第二个圆括号则是一个操作符。如下代码,将函数引用通过圆括号括起来是完全合法的:

不同的圆括号表示的意义不同,可能会有一点令人混乱。如果函数的调用操作符换成 ||,而不是 ( ),表达式 (...)|| 可能就不会那么让人感觉混乱。

最后,不管第一组圆括号中是什么内容,系统都会将其作为函数的引用看待。尽管在最新的环境中不需要第一组圆括号,但这样的用法还是没有问题的。

此时,如果我们在第一组圆括号内直接使用匿名函数,而不是变量名称,就是本文的第一个示例:(function(){...})();,如果我们在函数体内再加上一些语句,代码就变成如下这样了:

这段代码的最终结果是执行如下操作的单条语句表达式:

  • 创建一个函数实例
  • 执行该函数
  • 销毁该函数

此外,因为我们处理的是一个可以拥有闭包的函数,所以在函数调用的短暂过程中,是可以访问和声明与当前语句处于同一作用域的外部变量与参数的。这个简单的构造被称之为即时函数(immediate function)。

首先,让我们先了解一下作用域是如何与即时函数进行交互的。

临时作用域与私有变量

利用即时函数,我们建立一个有趣的封闭空间来做一些事情。由于函数是立即执行,其内部所有的函数,所有的变量都局限于其内部作用域。我们可以使用即时函数创建一个临时的作用域,用于存储数据状态。

注意:变量在 JavaScript 中的作用域依赖于定义变量的函数。通过创建一个临时函数,利用其特性,我们可以创建一个持有变量的临时作用域。

看一下,临时且独立的作用域是如何工作的。

创建一个独立的作用域

思考如下代码片段:

由于函数会立即执行,click 事件也会马上绑定。需要注意的一件重要事情是,上述代码为包含了 numClicks 变量的事件处理程序创建了一个闭包,该闭包使得 numClicks 变量可以在处理程序中持久化,并且可以被处理程序进行引用,除此之外就没有别的地方可以引用该变量了。

这也是一种最常见的即时函数使用方式。各功能所需要的变量都保存在闭包内,对其它地方都不可见(可以用它实现模块化)。

但是,最重要的是要记住,由于即时函数也是函数,它们的使用方式可以更加有趣,示例如下:

这是另外一个版本,与之前的版本相比,它更优雅,同时也更让人感到迷惑。

在本例中,我们再次创建一个即时函数,但这一次我们为即时函数返回一了一个值:作为事件处理程序的一个函数。就像其它表达式一样,该即时函数的返回值被传递到了addEventListener()方法。但是,我们创建的内部函数依然可以通过闭包获取 numClicks 变量的值。

该技巧从一个不同的视角看待作用域。在很多编程语言中,作用域是依赖于代码块的。但在 JavaScript 中,变量的作用域依赖于变量所在的闭包(closure)。

些外,利用即时函数,我们现在可以将作用域限制于代码块、子代码块以及各级函数中。像函数调用的参数一样,将代码的作用域限制在一个很小的单元内,其威力是巨大的,无疑也彰显了 JavaScript 语言的灵活性。

循环

即时函数另外一个有趣的地方是,它可以利用循环和闭包解决一些刺手的问题。考虑一下,如下这个常见的代码问题:

我们希望的结果是,点击第一个按钮的时候,输出“Button #0 was clicked.”,点击第二个按钮的时候,输出“Button #1 was clicked.”,我们点击第一个按钮,结果却是:
error_loop

在上述代码中,我们遇到了一个使用闭包和循环时常见的问题,也就是说,函数绑定之后,闭包抓取的变量(本例中是i)被更新了。这意味着,每个绑定的函数处理程序都会一直显示i最后的值,在这例子中,i的值变成了2。

这里涉及到一个要点:闭包记住的是变量的引用(reference),而不是闭包创建那一刻该变量的值。这是一个重要的区别,以致于很多人走了弯路。

修正这一问题的办法就是再用一个闭包与即时函数来包装语句,俗称以毒攻毒:

现在我们再点击第一个按钮,运行结果:
right_loop

通过在 for 循环内加入即时函数,我们可以将正确的值传递给即时函数(内部函数的闭包),进而让处理程序也得到正确的值。这意味着,在 for 循环的每次迭代的作用域中,i 变量都会重新定义,从而给 click 处理程序的闭传入我们期望的值。

发表评论

电子邮件地址不会被公开。 必填项已用*标注