JS闭包和构造函数对比研究
闭包(或闭包函数)的概念在JS社区讨论的频率异常的高,有人甚至 把闭包原理看作像是外语里语法的那样重要。闭包的确是JS开发的基石(之一),然而它的基础作用为何,可能还需进一步廓清。
有一定OOP基础的人一定会发现,闭包函数和构造函数(必须使用new操作符的JS函数)的性质和意义,有点相似。本文尝试分析它们的共通和不同的地方,以期达到深刻认识闭包的意义。
源起——计算功能 的制造种类
最近半个月一直在为重新出山面试作准备,在完成Promise(异步编程)的研习后,我又回到了JS 的基础复习中,其中的研习主题都是围绕着闭包、高阶函数、柯里化,和函数式编程。在这个过程,我开始酝酿着一种新看法,闭包,高阶函数,柯里化这些概念或技术都是以 「函数是一等公民」为前提的,它们都是在 「把函数作值处理的 」技术(请将它和 以数据作值处理相比较)。这种现象应该是 函数式编程范式的表现,目前我还没有对这种 「把函数作值处理的 」现象总结出完整的结论(初步使用「功能编程」概括之),研习还在进行中,不过我已经将它们进行归类,并且辨识出它各自的意义:
这里有一类首先引起了我特别的注意,就是创建新的一类。
闭包函数和构造函数的相似性
闭包的常识是说,它制作(返回)一个新函数,并且是带私有数据的新函数,从「把函数作值处理的 」的角度看,就是 新建一个「 函数值」注1,或者说新建一变量,它的值是 函数。如果是这个任务,我就想,构造函数 的意义不也是这样吗,只是构造函数返回的一个新对象实例(内部可this指向)。不过如果闭包返回的是带私有数据的函数,这也不是一个对象吗?
为了证明我的猜想,我实验一下将 使用闭包函数的实例改用构造函数 ,看看会怎么样。
带私有数据的事件处理函数
前端开发中,闭包函数使用常见一个例子就是 为DOM对象的交互事件填写 事件处理函数,而这个Event Handler是带参数数据的。例如页面提供了调整字体大小的交互功能(例子来自MDN ) ,分别用大中小三个按键的点击事件提供。每个按键的事件处理逻辑是相似的,只是字体样式不同。
<body>
<div id="app">
我们的文本尺寸调整按钮可以修改 body 元素的 font-size
属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。
<a href="#" id="size-s">小</a>
<a href="#" id="size-m">中</a>
<a href="#" id="size-x">大</a>
</div>
</body>
由于每个按键的事件处理逻辑是相似的,所以不必为它们编写单独的事件处理函数,我们可以把这些函数实例进行抽象,归纳出一个 「事件处理函数类」,这就是闭包派上用场了。
function makeSizerHandler(size) {
// 传进来的size 是属于一个本地私有变量
return function () {
document.body.style.fontSize = size + "px";
};
}
var sizes_hdlr = makeSizerHandler(12);
var sizem_hdlr = makeSizerHandler(14);
var sizex_hdlr = makeSizerHandler(16);
这个makeSizerHandler 就是闭包函数,这里非常简单的,只有一个参数和一条功能语句注2。然后,我们可绑定事件处理了:
document.getElementById("size-s").onclick = sizes_hdlr;
document.getElementById("size-m").onclick = sizem_hdlr;
document.getElementById("size-x").onclick = sizex_hdlr;
现在我们尝试使用构造函数改写这段代码。
用对象实例的方法替换闭包实例
JS构造函数是创建 对象(Object)的标准方法,虽然大部分情况大家推荐使用字面量创建。为了相区分,我们取一个名词:
function SizerHandler(size) {
this.size = size;
this.make = function () {
document.body.style.fontSize = size + "px";
};
}
我们如期使用new分别创建三个实例,并绑定:
var SHs = new SizerHandler(12);
var SHm = new SizerHandler(14);
var SHx = new SizerHandler(16);
document.getElementById("size-s").onclick = SHs.make;
document.getElementById("size-m").onclick = SHm.make;
document.getElementById("size-x").onclick = SHx.make;
注意到,我们并不能像闭包那样直接用实例名,而还要指定make方法。代码测试通过,功能和闭包一样。
this丢失的分析
这里我注意到一个点,当我在SizerHandler.make实现里使用实例的数据(this.size)时,测试不通过:
... this.make = function () { document.body.style.fontSize = this.size + "px"; ...
简单分析后,原因其实很简单,对象的方法被传递是不带父环境(this指向)的,当把SMs.make赋值给onclick后(就是将函数作值传递),make的执行父环境是那个按键对象,而不再是SMs了。
这就是this丢失。这里也有一个有趣的思考问题:
- 第一,为什么前面创建时使用初始size参数,可以测试通过?
- 第二,闭包例子为什么又可访问到数据呢?
这里就涉及了你怎么定了 「函数的完整形式」。例如当你初始化时就使用size,那么size就是make的形式定义的一部分,它不会丢失;但如果make是使用了对象实例其他属性数据,例如通用this.size引用,那make的形式就依赖了this。如果将make“移走”而不带这个this,它的形式是不完整的,故功能会不工作。这里的make 功能比较简单,它可不依赖this,但是如果make比较复杂必须依赖父对象时,我们如果要 将make 功能移走他用,则必须考虑make的完整形式,才能正确复用make的功能。如果我们不理解这个原理,则会被this丢失困扰。
而第二个,闭包的情况比较的特殊,闭包的涵义就是封装,强调形式的完整性,它将整个功能移走,而不仅仅语法上的一个函数,所以它可以访问到数据。
总结
闭包是返回函数的函数,与构造函数,在制作计算功能的任务上——用一个模板类派生具体功能实例——是一致的。我们可以初步总结:==闭包语法是制作带记忆(状态或私有数据)计算功能的轻便工具==,例如本文例子中制作带字体大小的点击事件处理函数。
闭包函数和构造函数的比较概括如下:
- 相同:都是用来创建新计算功能单元的「抽象类」
- 不同:闭包语法更轻便;构造函数使用语言支持的new操作符,是OO范式;闭包使用 函数作值传递,是FP范式
当然应用场景也有不同,计算功能比较精简时使用闭包,当功能比较复杂时,可能需要使用构造函数了。