深入学习JavaScript——闭包(Closure)

前言

作者虽然不是第一次写学习笔记了,但是系统性的专栏确实是第一回写,所以对这篇文章难度的预估量不够,酝酿了蛮久时间的。写之前一直在纠结一个问题,这类问题网上已经有各类大神做出各种很详细的分解了,还有必要写吗?
能让我有动力写的原因,一个是强烈的表达欲望,另一个是希望后来者能通过我这篇文章学到一点东西。基于这两点,我这篇文章的思路就有了:要写得尽可能详细甚至啰嗦,多用一些生活化的例子作比。当然,我这个行文思路也是参考借鉴了【张鑫旭】大神的。好了,接下来开启我的第一篇文章吧——关于 JavaScript 的闭包 (closures)。

正文前再啰嗦点什么

我希望看到这篇文章的人能够秉持一个思维习惯:和物理、数学等客观存在的知识点不一样的是,编程领域的知识点有相当大的部分具有主观性——意思就是,很多编程领域的知识首先是人根据现实的需求创造出来的,它不是客观就存在的。这就导致编程领域的一个现象:对于一种问题的解法,我们可以有很多不同角度的思路,这其中前端领域尤为突出。但它们都是人类所创造出来的概念。
这个思维会有助于我们去学习编程领域的知识点(至少我是这样)。回到我们这篇文章的主题,闭包,这个令许多 JS 新手头疼不已的问题,其实只是人们解决函数式编程问题用到的一个概念工具。什么是函数式编程?这个概念比闭包还难说清楚,计划专研后再后续写些文章详细总结。
现在我们就简单地把函数式编程与面向对象、面向过程编程视为同一层次的概念,它是一种编程的思维模式。而闭包只是实现函数式编程的高效工具之一。啰嗦了那么多,来看张明白直了的图吧~
图例

闭包的定义

上面说到闭包是实现函数式编程的工具之一。但是,闭包到底是什么,长什么样?《你不知道的 JavaScript》书中,将闭包定义为【函数在所定义的作用域之外的区域被调用】。听上去有点绕,我举个比较现实的例子:

闭包的函数定义与使用作用域分离,有点类似古代对军队的掌控和调度权力分离。在大多数情况下,古代军队是只忠于皇廷的;一旦恰逢大战,皇帝不好御驾亲征,这时会将军队的调度权临时给予将领。将领真正掌控了军队吗?正常情况下是没有的,他只有在沙场上调度军队的权力。我们可以看到,军队明面的掌管者是谁?皇帝。但实际的操纵者是谁?将领。
这种权力分离的过程是如何实现的呢?你可能知道的,虎符!虎符是实现这种权力传递,或者分离的概念性工具。

从某种意义上看,闭包和虎符一样,也是一种概念性工具。现在是不是能够勉强记住上面那句,【函数在所定义的作用域之外的区域被调用】?如果可以的话,现在举两个例子,请你判断是否为闭包,以及是否输出正常。最好思考一分钟,然后再看解释。

// example 1
function outter () {
    console.log("outter");
    return function inner () {
        console.log("inner");
    };
}
var p = outter();
p();
// example 2
function outter () {
    console.log("outter");
    function inner () {
        console.log("inner");
    }
    foo(inner);
}
function foo (fn) {
    fn();
}
outter();

上面两个例子,你有什么看法?事实上,这两个都是《你不知道的 JavaScript》里提及的闭包例子。
第一个例子,outterinner作为返回值传递给pp在全局作用域执行时,inner的定义所在作用域与执行作用域不同,符合上面提到的闭包定义。
第二个例子,inner作为参数被传递给foo执行,同样的我们可以看到,inner的定义所在域与执行域不同,虽然它们都是outter函数作用域的子集。
不知看到此处的你,对闭包的理解是否清晰些了?如果你之前看过其它博客文章,它们对闭包的定义可能与本文不一致。比如说,在阮一峰这篇博文里,对闭包的定义是【能够读取其它函数内部变量的函数】。可能网上还有其它的定义。事实上,这些定义本质上都是一样的,它们只不过从某个特定角度描述闭包的特性。就像虎符,虽然在各个朝代都有出现,但是它在每个时期的形状是不尽相同的。挑一个你认为最好记的,加上辅助理解的实例就好。

闭包的作用

实际上,我更愿意将阮一峰对闭包的定义——【能够读取其它函数内部变量的函数】,视为闭包的作用之一。但是,它的作用远不止这个。
回到之前提及的函数式编程,它的一大特征就是【函数是一等公民】。这句话怎么理解?大概地说,函数式编程提倡用函数来实现,传统面向对象中只有类和对象才能实现的功能。之前提到,闭包是实现函数式编程的工具之一,其实用处主要就在这里。
闭包如何实现函数式编程呢?其实很简单,既然闭包可于读取函数的内部变量,换个角度想就是,闭包可以实现函数变量或方法的公有化。有些同学可能知道,传统面向对象中,只有对象才有私有和共有变量、方法之分,函数内部不仅不能定义函数,也不能主动暴露内部变量。JS 中函数内部是可以定义函数的,相当于有了内部方法。有了闭包,JS 中的函数就和对象很像啦,这就很符合函数式编程的理念了~
然而,说了那么多干巴巴的定义,对于函数如何当对象使用,你可能还是很懵。没关系,举几个实例来辅助说明。

// example 3
function people (_name, _age) {
    var name = _name;
    var age = _age;
    return function get () {
        console.log('name: ' + name);
        console.log('age: ' + age);
    };
}
var get_info = people("Lee", 20);
get_info();   // name: Lee; age: 20

上述例子在一定程度上体现了函数被当作对象的特点。然而,有人可能会钻一个牛角尖:我已经知道它返回的是一个函数了,你只不过是在执行这个函数而已。首先恭喜你,能提出这个疑问,代表你至少看懂上一节内容了;其次,这个示例的重点不在函数的执行,而在这个函数输出了不属于它的变量数据。这在某种程度上,上文也有提到,实现函数变量或方法的公有化
对 JS 有点了解的可能会问:JS 中不是有垃圾回收机制吗,为什么people执行后它的内部变量没有被回收?这个问题问得相当好,有一段时间我也很疑惑这点。下面再举一个很经典的计数器例子,我们来看看为什么会这样。

// example 4
function counter () {
    var count = 0;
    console.log("init: " + count++);
    return function increase () {
        count += 2;
        console.log("increase: " + count);
    }
}
var increase1 = counter();    // init: 0
var increase2 = counter();    // init: 0
increase1();    // increase: 3
increase1();    // increase: 5
increase2();    // increase: 3

这个例子是计数器的变形,之所以在counter内部域中做一个输出,是为了观察执行increase时是否会执行counter内部的语句。
可以看到,其一,increase执行时只用到counter的变量,并不执行某些特定的语句。但假如引用了另一个函数m,肯定会执行m内部的语句。其二,在代码的运行周期内,count变量一直保留;只要愿意,可以执行若干次increase。这不符合 JS 垃圾回收的机制。其三,两个变量 (increase1&increase2) 引用的计时器变量count不同。在 JS 语境下,它们其实是两个对象。
上述疑问中,一大家想一下就能明白了,三不是本文的重心,以后会再提到,重点是疑问二。这里其实涉及到 JS 的函数传递和词法作用域。在 JS 中,【函数是一等公民】还体现在:函数可以作为参数、返回值等进行传递。上述示例中,increase1获取了counter返回的函数increase,此时这个函数保留有原父函数counter内部变量的引用。为了代码引用非空,JS 对此会进行特殊处理,【保留原父函数的词法域】,但不会再执行。

总结

写到这里,这篇啰里巴嗦的文章总算是到尾声了。现在来做一个总结吧~
本文我们认识了闭包,那么本文对闭包的定义是什么?这里不打出来,你自己小声默念一遍,想不起来就想想虎符的例子。
第二个,闭包的作用?最大的作用当然是保证【函数是一等公民】的地位啦。但是这个太抽象了,回想一下例子,我们可以得到两个特殊作用,或者说特点。

  • 能够用于读取函数内部变量和方法,模拟对象
  • 保留父函数的词法域。需要注意的是,滥用这个特点会导致内存泄露。

参考

学习 JavaScript 闭包 —— 阮一峰
到底什么是闭包 —— 知乎用户 Agile2
《你不知道的 JavaScript(上卷)》

猜你喜欢

转载自www.cnblogs.com/ChanWunsam/p/10704499.html