JavaScript 闭包
概括
闭包是 JavaScript 编程语言中非常强大的机制。默认情况下,JavaScript 中对象的所有成员都是公共的。但是,闭包机制使对象可以拥有私有成员等等。在本教程中,我们将了解闭包以及在 JavaScript 代码中使用闭包的好处。
定义
那么,闭包是什么? 《JavaScript:精髓》一书的作者 Douglas Crockford在这里使用了一些闭包机制的很好的例子。他对闭包的定义类似于以下内容:
闭包是一个内部函数,它始终可以访问其外部函数的变量和参数,即使外部函数已经返回。
高阶函数
让我们考虑以下代码:
function counter(initValue) {
var currentValue = initValue;
var increment = function(step) {
currentValue += step;
console.log('currentValue = ' + currentValue);
};
return increment;
}
var incrementCounter = counter(0);
incrementCounter(1);
incrementCounter(2);
incrementCounter(3);
在 JavaScript 语言中,函数是对象,因此它们可以作为参数传递给其他函数,也可以从其他函数返回并分配给变量。
在我们的示例中,我们创建了计数器函数,它返回增量函数并将其分配给incrementCounter变量,以便该变量包含对增量函数的引用(JavaScript 中的对象是通过引用复制的,而不是通过值复制的)。
该引用使我们能够在计数器函数范围之外调用增量函数,因此在我们的例子中,我们从全局范围调用它。除此之外,行var increaseCounter = counter(0);初始化currentValue变量。
此后每次调用,incrementCounter都会作为内部函数(由increment引用)的引用。因此,incrementCounter(1)这一行实际上调用了参数为 1 的increment函数。由于该函数是一个闭包,因此它仍然可以访问其外部函数的变量和参数,尽管我们在外部函数之外调用了它。因此,currentValue将增加 1,其值将变为 1。在后续函数调用中,currentValue将分别增加 2 和 3,因此代码的输出将是:
currentValue = 1
currentValue = 3
currentValue = 6
在 JavaScript 中,函数的局部变量将在函数返回后被销毁,除非至少有一个引用。在我们的示例中,currentValue在增量函数中被引用,而增量函数在全局范围内被引用。因此,currentValue和增量将在整个脚本执行完毕之前不会被销毁。这就是为什么即使在计数器函数返回后,我们仍可以从全局范围调用增量函数的原因;增量是通过引用调用的,因为它存储在增量计数器函数中。
通过对象关闭
计数器函数还可以返回包装到对象中的多个函数。如果我们有减量函数并想返回它,那么可以使用以下代码来实现:
function counter(initValue) {
var currentValue = initValue;
var increment = function(step) {
currentValue += step;
console.log('currentValue = ' + currentValue);
};
var decrement = function(step) {
currentValue -= step;
console.log('currentValue = ' + currentValue);
}
return {increment: increment,
decrement: decrement};
}
返回的项目具有increment和decrement属性,其值分别是increment和decrement函数。这样,我们可以将这些内部函数暴露给全局范围,以便我们可以从中调用它们:
var myCounter = counter(0); // myCounter now refers to the object returned by counter
myCounter.increment(1); // call the appropriate "property" function using dot notation
myCounter.increment(2);
myCounter.decrement(1);
myCounter.increment(3);
myCounter.decrement(2);
如果返回多个函数,则必须将它们包装到对象中,如上例所示。即使外部函数返回单个内部函数,也可以将输出包装到对象中,但这通常有点过头了。
我们还可以在计数器函数内部添加一些函数,但这些函数我们不想暴露给全局(外部)作用域。例如,我们可以有一个“私有”函数来记录currentValue:
function counter(initValue) {
var currentValue = initValue;
var logCurrentValue = function() {
console.log('currentValue = ' + currentValue);
}
var increment = function(step) {
currentValue += step;
logCurrentValue();
};
var decrement = function(step) {
currentValue -= step;
logCurrentValue();
}
return {increment: increment,
decrement: decrement};
}
在这种情况下,logCurrentValue函数无法从全局范围访问,因为它不是由计数器函数返回的。它只能在增量和减量函数中使用,但此函数仍然是私有的,因为不能从外部调用它。
此外,currentValue和initValue变量对于计数器函数对象来说是私有的。简而言之,这就是我们在 JavaScript 中模拟私有成员的方法——通过使用作用域限制访问。
多个对象引用
让我们看看如果我们创建多个函数对象,这个机制是如何工作的。例如,让我们使用counter创建两个名为myCounter1和myCounter2 的对象,并调用它们的属性函数:
var myCounter1 = counter(0);
var myCounter2 = counter(3);
myCounter1.increment(2);
myCounter2.increment(2);
myCounter1.decrement(1);
myCounter2.decrement(1);
在代码的前两行中,我们实际上创建了 2 个不同的对象,myCounter1和myCounter2。这些对象具有相同的属性,increment和decrement ,它们是对增量和减量函数的引用。myCounter1对象是通过使用参数0调用计数器函数创建的,而myCounter2通过使用参数3调用计数器函数创建的。这意味着增量和减量函数在外部作用域中将具有不同的currentValue变量值,因此它们对这两个对象的调用将产生不同的结果。在这种情况下,我们有两个不同的增量和减量函数,因为我们创建了两个不同的对象,每个对象都有一对具有自己作用域的函数。
这是一个相当令人困惑的极端情况,但它使代码更容易理解。两个变量引用指向不同的对象,因为myCounter1和myCounter2是使用对counter的单独调用来实例化的。因此,它们对increment和decrement 的调用也是分开的。
上述代码的输出将是:
currentValue = 2;
currentValue = 5;
currentValue = 1;
currentValue = 4;
用例:超时
闭包主要用于回调,例如超时、事件处理程序等,以及模块中。
让我们考虑以下代码:
for (var i = 0; i < 5; i++) {
console.log(i);
}
该代码的输出是按顺序从0到4的数字。
让我们稍微改变一下:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 3000);
}
上述代码的输出将完全不同;数字 5 将被记录 5 次。
让我们分析一下这段代码。作为参数传递给setTimeout函数的函数将在第一次循环迭代后 3 秒执行。因此,在其外部作用域中的变量i的值(此函数也是一个闭包)将在执行之前变为5,因为所有 5 次迭代肯定会在这 3 秒内完成。该函数将执行 5 次,每次迭代执行 1 次,并且它将记录5次。
换句话说,当超时结束时,i的值将为 5,并且该值将被记录下来。
使用闭包来修复这个问题
为了获得所需的输出,我们需要添加一个闭包,因此我们的代码应该是这样的:
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~