《JavaScript忍者秘籍》挥舞函数

阅读忍者秘籍。

简介

包是一个函数在创建时允许该自身函数访问并操作该自身函数之外的变量时所创建的作用域。简而言之,闭包可以让函数访问所有的变量和函数,只要这些变量和函数存在于该函数声明时的作用域内就j行。

要记住:声明的函数在后续什么时候都可以被调用,即便是声明时的作用域消失之后。

一个简单的示例

注意:这些闭包结构不是可以轻易看到的,这样的话就会导致在存储和引用信息方面会直接影响性能 每个通过闭包进行信息访问的函数都有一个“锁链”,如果我们愿意,可以在它上面附加任何信息,但是你要时刻注意这方面的开销

使用闭包

私有变量

闭包的一种常见用法是封装一些信息作为“私有变量”,即限制作用域,以达到传统的私有变量的特性,通过闭包可以实现一个类似的功能。

实际的例子

回调和计时器

在这种情况下,函数都是在后期、未指定的时间进行异步调用,在这种情况下,经常需要访问外部数据。

回调的例子

计时器的例子

上述例子,还说明了一个重要的概念。函数在闭包里执行的时候,不仅可以在闭包创建的时刻点上看到这些变量的值,我们还可以对其进行更新。简而言之,闭包不是在创建的哪一个时刻点的状态的快照,而是一个真实的状态封装,只要闭包存在,就可以对其中的变量和函数进行修改。

绑定函数上下文

使用 callapply 方法来操作函数上下文,虽然有用,但是在面向对象的代码中,有潜在的危害性。

简单的例子

上述例子来源于一个流行 JavaScript 库的某个函数的简化,原始版本如下:

1
2
3
4
5
6
7
8
9
Function.prototype.bind = function() {
    var fn = this;
    var args = Array.prototype.slice.call(arguments);
    var object = args.shift();

    return function() {
        return fn.apply(object, args.concat(Array.prototype.slice.call(arguments)));
    };
};

完整的例子

要意识到,这里的 bind 函数,并不意味着它是 apply()call() 的一个替代方法。记住,该方法的潜在目的是通过匿名函数和闭包控制后续执行的上下文。这个重要的区别使得 apply()call() 对事件处理程序和定时器的回调进行延迟执行特别有帮助。内部原理就是下面要说的柯里化。

偏应用函数

在函数调用之前,可以预先传入一些函数,返回一个含有预处理参数的新函数,以便后期调用。这种在一个函数中首先填充几个参数(或函数),然后返回一个新函数的技术称为柯里化。

实际的例子

以上例子展示了使用闭包减少了代码的复杂性。

函数重载

有两种方式可以做到:在用户毫无感知的情况下,完全操作一个函数的内部行为。

  1. 修改现有的函数(不需要闭包)
  2. 基于现有的函数创建一个自更新的新函数。

缓存记忆

第一种方式我们已经实现过了,就好比上面的偏应用函数一样,需要附加一个额外的方法,才能实现功能,但是调用者根本记不住。

1
document.body.addEventListener => document.body.addEventListener.partial

现在我们来看看闭包是如何解决这一问题的。

实际的例子

每个函数都有自己的上下文,所以函数从来都不是闭包的一部分。但是可以通过创建一个变量引用到上下文上,从而可以将上下文编程闭包的一部分。

缺点在于:如果过多地利用闭包修改函数的逻辑,那会让函数变得不可扩展,这显然也是不可取的。

函数包装

这是一种封装函数逻辑的技巧,用于在单个步骤内重新创建新函数或继承函数。

实际的例子

上述例子表明了我们可以用一种比较隐蔽的方式重写任何对象的方法中已有的功能。

即时函数

你可能已经在很多地方见到过如下的即时函数:

1
(function(){})()

我们知道,可以通过函数名加圆括号的语法方式调用任意的一个函数,在这里,我们使用一个引用函数实例的表达式作为函数的名称。本质和如下代码一样:

1
2
var someFunction = function() {}
result = someFunction();

只是在这里,函数调用操作符 () 应用在整个表达式上,所以需要用圆括号将该表达式括起来,即用圆括号来划定表达式的范围。(请多读几遍) 所以如上的代码可以这样写:

1
2
var someFunction = function() {}
result = (someFunction)(); // 不管第一组圆括号中是什么内容,都会将其作为函数的引用进行支持。

注意:不同的圆括号表示的意义不同。变量在 JavaScript 中的作用域依赖于定义变量的函数(即变量的作用域依赖于变量所在的闭包)。

你看到的那个最简单的即时函数所表示的操作如下:

  1. 创建一个函数实例。
  2. 执行该函数。
  3. 销毁该函数(因为语句结束后,没有任何引用了)。

临时作用域和私有变量

创建一个独立的作用域

1
2
3
4
5
6
7
(function() {
    // 闭包使得 numClicks 变量可以在处理程序中持久化。
    var numClicks = 0;
    document.addEventListener("click", function(){
        alert(++numClicks);
    }, false);
})();

上述代码的另外一种版本:

1
2
3
4
5
6
document.addEventListener("click", (function() {
    var numClicks = 0;
    return function() {
        alert(++numClicks);
    };
})(), false);

利用这个简单的构造(即时函数),我们可以将作用域限制于代码块、子代码或各级函数中。

通过参数限制作用域内的名称

因为即时函数也是函数,所以也可以传递参数,如下:

1
2
3
(function(what) {
    alert(what);
})("Hi there!");

这种功能的一个应用就是避免已有的特殊变量被更改了引用,比如在有 jQuery 的页面中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<body>
    <script type="text/javascript">
    $ = function() {
        alert("not jQuery!");
    };

    (function($){
        // 因为 $ 参数会成为函数体内所创建内部函数的闭包的一部分,包括给
        // jQuery 的 on() 方法所传递的事件处理程序,所以即便是在即时函数执行并消失后,
        // 还是能正常工作。
        $('img').on('click', function(event) {
            $(event.target).addClass('clickedOn');
        })
    })(jQuery);
    </script>
</body>

使用简洁名称让代码保持可读性

很多时候,你会这样做:

1
var short = Some.long.reference.to.something;

但是这样做,会在当前作用域中引入不必要的名称,这是需要避免的。

1
2
3
4
5
6
(function (v) {
    Object.extend(v, {
        href: v._getAttr,
        src: v._getAttr
    });
})(Element.attributeTranslations.read.values);

这个技巧对没有延迟调用的循环遍历来说很有用,但是当循环中延迟调用的时候,比如循环给事件绑定处理程序等就需要下面的方法了。

循环

实际的例子

注意:闭包记住的是变量的引用–而不是闭包创建时刻该变量的值。

类库包装

闭包和即时函数对于开发类库尤其重要,因为不希望一些不必要的变量去污染全局命名空间,尤其是那些临时变量,尽可能的保持私有,并且可以有选择性的让一些变量暴露到全局命名空间中。如下所示:

1
2
3
4
5
6
7
8
9
(function() {
    // 因为不能保证全局的 jQuery 变量不会被改变,我们将其
    // 赋值给了一个局部变量 jQuery,强制其保持在即时函数的作用域内。
    var jQuery = window.jQuery = function() {
        // Initialize
    };

    // ....
})();

当你只需要输出一个变量的时候,可以采用如下的写法:

1
2
3
4
5
6
7
8
var jQuery = (function() {
    function jQuery() {
        // Initialize
    }
    // ...

    return jQuery;
})();

总结

  1. 闭包特别有用,包括私有变量定义和回调的使用。
  2. 闭包的一些应用,如:强制设置函数上下文、偏应用函数、重载函数、即时函数。所有的这些,可以让我们能够细粒度地控制变量作用域。
updatedupdated2023-12-052023-12-05