javascript闭包
在Node.js中,闭包以各种形式广泛使用,以支持Node的异步和事件驱动的编程模型。 通过对闭包有很好的了解,可以确保开发的应用程序的功能正确性,稳定性和可伸缩性。
闭包是将数据与作用于数据的代码相关联的一种自然方式,而继续传递是主要的语义样式。 使用闭包时,即使在逻辑上退出了封闭范围,在封闭范围内定义的数据元素也可被该范围内创建的函数访问。 在函数是一类变量的语言(例如JavaScript)中,此行为非常重要,因为函数的生命周期决定了函数可见的数据元素的生命周期。 在这种环境下,无意中在内存中保留比预期多得多的数据非常危险。
本教程研究了在Node中使用闭包的三种主要用例:
- 完成处理程序
- 中介功能
- 监听器功能
对于每种用例,我们都提供示例代码,并指示闭包的预期寿命以及该寿命期间保留的内存。 此信息可帮助您设计JavaScript应用程序,深入了解这些用例如何影响内存使用率,从而避免应用程序中的内存泄漏。
闭包和异步编程
如果您熟悉传统的顺序编程,则在首次尝试理解异步模型时可能会问以下问题:
- 如果一个函数被异步调用,那么如何确保调用后(或周围)的代码可以处理调用时作用域中可用的数据? 换句话说,如何实现其余代码,具体取决于异步调用的结果和副作用?
- 进行异步调用之后,程序将继续执行与异步调用无关的代码,那么您如何稍后返回到原始调用范围以在异步调用完成后继续进行操作?
闭包和回调是这些问题的答案。 在最常见和最简单的用例中,异步方法将回调方法(具有关联的闭包)作为其参数之一。 该函数通常在异步方法的调用站点处以内联方式定义,并且该函数可以访问包围该调用站点的范围的数据元素(局部变量和参数)。
以下面JavaScript代码为例:
function outer(a) {
var b= 20;
function inner(c) {
var d = 40;
return a * b / (d c);
}
return inner;
}
var x = outer(10);
var y = x(30);
这是实时调试会话中相同代码的快照:
inner
函数在第17行(前面清单的第11行)中调用,并在第11行(清单的第5行)执行。 在第16行(清单的第10行),调用了outer
函数(返回inner
函数)。 如屏幕截图所示,在第17行调用inner
函数并在第11行执行时,它可以访问其局部变量( c
和d
) 以及在outer
函数中定义的变量( a
和b
)—即使在第16行完成对outer
函数的调用时,就退出了outer
函数的作用域。
“为了避免内存泄漏,它的回调方法保持多长时间到达时和理解是很重要的。 ”
回调方法处于可以被调用的状态(即,从垃圾回收的角度来看是可以到达的),因此它使它可以访问的所有数据元素保持活动状态。 为了避免内存泄漏,重要的是要了解回调方法在该状态下保持的时间和时间。
在较高的级别上,关闭通常在至少三种用例中起作用。 在这三个方面,基本前提是相同的:一小段可重用代码(可调用函数)与上下文一起工作并有选择地保留上下文的能力。
用例1:完成处理程序
在完成处理程序模式中,函数( C1
)作为参数传递给方法( M1
),并且在M1
完成时将C1
作为完成处理程序调用。 作为模式的一部分, M1
的实现可确保不再需要保留对C1
的引用。 C1
通常在调用M1
的范围内需要一个或多个数据元素。 创建C1
时,定义了用于访问此范围的闭包。 一种常见的方法是使用内联定义的匿名方法,其中调用了M1
。 结果是C1
的闭包,该闭包提供对M1
可用的所有变量和参数的访问。
一个示例是setTimeout()
方法。 当计时器到期时,将调用完成功能,并清除对计时器保留的对完成功能( C1
)的引用:
function CustomObject() {
}
function run() {
var data = new CustomObject()
setTimeout(function() {
data.i = 10
}, 100)
}
run()
完成函数使用调用setTimeout
方法的上下文中的data
变量。 即使在run()
方法完成之后,也可以为完成处理程序创建的闭包引用CustomObject
,而闭包不会被垃圾回收。
记忆保留
定义完成函数( C1
)时创建闭合上下文,并且该上下文由创建C1
作用域中可访问的变量和参数组成。 C1
的闭包将保留到以下两种情况:
- 完成方法将被调用并完成运行或清除计时器。
- 没有其他引用
C1
情况。 (对于匿名函数,如果满足此列表中的先前条件,则不会发生其他引用。)
通过使用Chrome开发人员工具 ,我们可以看到代表计时器的Timeout
对象通过_onTimeout
字段引用了完成函数(传递给setTimeout
的匿名方法):
当计时器生效时, Timeout
对象, _onTimeout field
和闭包函数都通过对它们的单个引用(即系统中待处理的超时事件)全部保存在堆中。 当计时器触发并且后续回调完成时,事件循环中的未决事件将被删除。 这三个对象都不再可访问,并有资格在随后的垃圾收集周期中进行收集。
清除计时器后(通过clearTimeout
方法),将从_onTimeout
字段中删除完成函数,并且-只要没有其他对该函数的引用,就可以在后续的垃圾收集周期中收集该完成函数,即使该Timeout
对象也是如此。之所以保留,是因为主程序保留了对它的引用。
在此屏幕截图中,比较了在计时器触发之前和之后进行的堆转储 :
#New列显示在转储之间添加的新对象,而#Deleted列显示在转储之间收集的对象。 突出显示的部分显示CustomObject
在第一个转储中存在,但已被收集,在第二个转储中不可用,从而释放了12个字节的内存。
在这种模式下,执行的自然流程是仅保留内存,直到完成处理程序( C1
)完成其“完成”方法( M1
)的工作为止。 结果是,只要应用程序调用的方法及时完成,您就不必特别注意避免内存泄漏。
当设计实现此模式的函数时,请确保在触发回调时清除对回调函数的所有引用。 这样,您可以确保在内存保留方面满足使用功能的应用程序的期望。
用例2:中介功能
在某些情况下,无论是以异步方式还是同步方式创建数据,您都需要能够以更重复,迭代和越界的方式处理数据。 在这种情况下,您可以返回一个中间函数 ,该函数可以被调用一次或多次以访问所需的数据或完成所需的计算。 与完成处理程序一样,您可以在定义函数时创建闭包,闭包提供对定义函数的作用域中可用的所有变量和参数的访问。
这种模式的一个示例是数据流传输,其中服务器返回大量数据,并且为到达的每个数据块调用客户端的数据接收者回调。 因为数据流是异步的,所以操作(例如数据累积)必须是迭代的,并且以越界方式发生。 下面的程序说明了这种情况:
function readData() {
var buf = new Buffer(1024 * 1024 * 100)
var index = 0
buf.fill('g') //simulates real read
return function() {
index++
if (index < buf.length) {
return buf[index-1]
} else {
return ''
}
}
}
var data = readData()
var next = data()
while (next !== '') {
// process data()
next = data()
}
在这种情况下,只要data
变量仍在作用域内, buf
就会保留。 buf
缓冲区的大小会导致保留大量内存,即使这对于应用程序开发人员而言可能并不明显。 我们可以使用Chrome开发人员工具看到这种效果,如while
循环完成后的快照所示:即使不再使用大缓冲区,它也会保留下来。
记忆保留
即使在应用程序完成使用中间函数之后,对该函数的引用也使关联的闭包保持活动状态。 为了允许收集数据,应用程序必须覆盖此引用,例如,通过将引用设置为中介函数,如下所示:
// Manual cleanup
data = null;
此代码允许关闭上下文被垃圾回收。 以下是在将data
设置为null
之后从堆转储中截取的以下屏幕截图,其中显示了手动无效化允许垃圾回收保留的数据:
高亮显示的行表示缓冲区已被收集并且其相关的内存已释放。
通常可以构造中间函数,以便它们限制潜在的内存泄漏。 例如,允许增量读取大数据集的中介程序可能会删除对返回的数据部分的引用。 但是在这些情况下,重要的是,对于可能通过中介功能以外的其他方式访问该数据的应用程序其他部分,不必担心此方法。
创建实现中介模式的API时,请小心记录内存保留特征,以便使用者了解确保所有引用均无效的需求。 更好的是,如果可能的话,实现您的API,以便在中间函数中不再需要保留的数据时可以释放它们。
例如,本节上一个示例中的函数可以重写为:
return function() {
index++;
if (index < buf.length) {
return buf[index-1]
} else {
buf = null
return
}
}
此版本可确保在不再需要大缓冲区之后,可以对其进行收集。
用例3:侦听器功能
一种常见的模式是注册侦听特定事件发生的函数。 尽管在异步编程中非常方便,但此注册会使侦听器函数以及任何关联的闭包上下文“转义”到事件发射器的内部缓存中。 只要可以生成和处理事件,并且事件拦截器模块知道何时停止事件发出并清除侦听器,就可以了。 当应用程序的侦听器功能的生命周期变得不确定或未知时,就会出现这种风险。 因此,侦听器功能最有可能导致内存泄漏。
“侦听器功能最容易导致内存泄漏。 ”
多数流/缓冲方案使用此机制来缓存或累积在外部方法中定义的瞬时数据,而访问是在闭包函数中完成的。 考虑以下示例:
var EventEmitter = require('events').EventEmitter
var ev = new EventEmitter()
function run() {
var buf = new Buffer(1024 * 1024 * 100)
var index = 0
buf.fill('g')
ev.on('readNext', function nextReader() {
var byte = buf[index]
index++
process(byte)
});
}
记忆保留
下面的屏幕快照是在调用run()
方法之后拍摄的,显示了如何为大缓冲区buf
保留内存。 从统治者树中可以看到,大缓冲区由于与事件关联而保持活动状态:
即使在读取所有数据之后,由回调函数(侦听器)保留的数据也会保持活动状态,更不用说外部函数早已返回的事实。 因为函数( nextReader
)访问缓冲区,所以将在运行时中创建并维护一个关闭上下文,只有当该函数与事件分离并且不再被引用时,该上下文才会被清除。
// Once the purpose of ‘readNext’ event is met,
// remove the event from the Emitter
ev. removeListener(‘readNext’, nextReader);
当安装生命周期为以下的侦听器时,会产生风险:
- 既不在您的控制范围内,也不是众所周知的。
- 在您的监督和控制下,但关闭是匿名的,因此无法引用和删除。
尽管Node核心库实现了许多事件发射器(例如流)及其使用者(例如http),但是它们的生命周期定义是众所周知的。 同样,在设计具有事件发射器和事件使用者的应用程序和库模块时,您应该花些时间了解上述行为,并确保事件的生命周期得到了很好的定义,并且可以在不再需要它们时将其注销。 。
“将功能注册为侦听器时,请确保在应用程序的适当生命周期阶段将其注销。 ”
此用例的一个著名示例是典型的HTTP服务器实现:
var http = require('http');
function runServer() {
/* data local to runServer, but also accessible to
* the closure context retained for the anonymous
* callback function by virtue of the lexical scope
* in the outer enclosure.
*/
var buf = new Buffer(1024 * 1024 * 100);
buf.fill('g');
http.createServer(function (req, res) {
res.end(buf);
}).listen(8080);
}
runServer();
尽管此示例显示了使用内部函数的便捷方法,但是请注意,只要服务器对象还处于活动状态,则回调函数(因此也就是缓冲区对象)仍处于活动状态。 仅当服务器关闭时,该对象才有资格被收集。 您可以在下面的屏幕快照中看到缓冲区由于服务器请求侦听器的使用而保持活动状态:
课程是,对于任何保留大量数据的侦听器,您都需要了解并记录该侦听器所需的寿命,并确保在不再需要该侦听器时将其注销。 还建议确保侦听器由于其通常较长的生命周期而在每次调用时保留尽可能少的数据量。
结论
闭包是强大的编程构造,可用于以更灵活和越界的方式实现代码与数据之间的绑定。 但是,作用域语义对于习惯于Java或C ++等较旧语言的程序员可能并不熟悉。 为了避免内存泄漏,了解闭包的特性及其生命周期非常重要。
翻译自: https://www.ibm.com/developerworks/web/library/wa-use-javascript-closures-efficiently/index.html
javascript闭包