这个JavaScript闭包函数如何在没有全局变量的情况下重复使用一个对象?

12
我将尝试更深入地理解Javascript,重新阅读JavaScript: The Good Parts。这里有一个疑问:
假设我想要避免使用全局变量,因为它们是有害的,那么我应该这样写:
var digit_name = function(n) {
 var names = ['zero','one','two','three'];
 return names[n];
}

D.Crockford声称这是慢的,因为每次调用函数时,都会进行names的新实例化。因此,他采用闭包解决方案,做法如下:
var digit_name = function () {
  var names = ['zero', 'one', 'two', 'three'];
  return function (n) {
    return names[n];
  }
}();

这样做会将names变量存储在内存中,因此每次调用digit_name时都不会被实例化。

我想知道为什么?当我们调用digit_name时,为什么第一行被“忽略”了?我漏掉了什么?这里真正发生了什么?
我不仅基于书中的例子,还基于这个video(第26分钟)
(如果有人想到更好的标题,请提出适当的建议...)

Crockford真的建议这样做吗?在哪一页?如果您的代码最后有一个(),意味着digit_name获取外部函数的返回值,即内部函数,那么这将更有意义。 - apsillers
我的错误。忘记在结尾处添加();。对不起,伙计们。 - Nobita
“我想避免使用全局变量,因为它们是有害的。” - 不,它们并不比刀子更邪恶 - 重要的是你如何使用它们。 (即使这样,不适当使用全局变量也是不合适的,而不是邪恶的。)但无论如何,为了避免使用全局变量而开始编写的代码立即创建了一个全局变量(digit_name)。 - nnnnnn
@nnnnnn:我实际上是引用了D.Crockford的话,我在两个会议上听到他说过这句话 :) - Nobita
2
是的,Crockford先生似乎看待所有事情都是非黑即白的。我认为灰色地带让他感到紧张。(并不是说他没有很多好建议——问题在于弄清楚哪些建议仅仅是他个人的偏好呈现为“事实”而已。) - nnnnnn
3个回答

12

我相信你想把你的第二个例子函数变成一个立即执行函数(即自调用函数),像这样:

var digit_name = (function () {
  var names = ['zero', 'one', 'two', 'three'];
  return function (n) {
    return names[n];
  }
})();

The distinction involves scope chain with closures. Functions in JavaScript have scope in that they will look up into parent functions for variables that are not declared within the function itself.
当你在JavaScript函数内部声明一个函数时,会创建一个闭包,这个闭包会划分出一个作用域。
在第二个例子中,digit_name 被设置为一个自执行函数。这个自执行函数声明了 names 数组并返回一个匿名函数。
因此,digit_name 变成了:
function (n) {
  //'names' is available inside this function because 'names' is 
  //declared outside of this function, one level up the scope chain
  return names[n];
}

从您的原始示例中,可以看出names是在返回的匿名函数(现在是digit_name)的上一级作用域链中声明的。当该匿名函数需要使用names时,它会沿着作用域链向上查找,直到找到声明的变量,在这种情况下,names在作用域链的上一级被找到。
关于效率: 第二个示例更有效,因为names仅在自执行函数触发时声明(即var digit_name =(function(){...})();)。当调用digit_names时,它将查找作用域链,直到找到names
在您的第一个示例中,每次调用digit_names时都会声明names,因此效率较低。
图形化示例:

您提供的来自Douglas Crockford的例子在学习闭包和作用域链时是一个相当困难的例子——很多内容都压缩在少量的代码中。我建议看一下闭包的可视化解释,比如这个:http://www.bennadel.com/blog/1482-A-Graphical-Explanation-Of-Javascript-Closures-In-A-jQuery-Context.htm


当你在JavaScript中声明一个函数内部的函数时,这会创建一个闭包。当你调用另一个函数中声明的函数时,每次调用都有自己的闭包... - nnnnnn
在另一个函数内声明的函数调用不会创建新闭包。您将重复使用该闭包(每次调用都会有自己的私有作用域),但不会创建新的闭包。 - Elliot B.
非常抱歉,你是正确的。我本意是说每次调用创建新闭包的“包含”函数时都会创建一个新闭包。我这么说更多的是为了澄清而不是纠正——你可以说内部函数在调用包含函数之前并没有被声明,但对于概念上的新手来说,他们可能会看代码并认为一切都已经被声明了... - nnnnnn

3

如果给出的示例仍然令人困惑,那么这不是一个答案,而是一个澄清。

首先,让我们澄清一下。在代码中,digit_name不是你看到的第一个函数。该函数只是创建另一个函数的返回值(是的,你可以像返回数字、字符串或对象一样返回函数,实际上函数也是对象):

var digit_name = (
    function () { // <------------------- digit name is not this function

        var names = ['zero', 'one', 'two', 'three'];

        return function (n) { // <------- digit name is really this function
            return names[n];
        }
    }
)();

为了简化示例并仅说明闭包的概念,而不混淆自调用函数之类的内容(您可能尚未熟悉),您可以将代码重写如下:

function digit_name_maker () {
    var names = ['zero', 'one', 'two', 'three'];

    return function (n) {
        return names[n];
    }
}

var digit_name = digit_name_maker(); // digit_name is now a function

需要注意的是,尽管names数组是在digit_name_maker函数中定义的,但它仍然可以在digit_name函数中使用。基本上,这两个函数共享该数组。这就是闭包的含义:在函数之间共享变量。我喜欢把它看作一种私有全局变量——感觉像全局变量,因为所有函数都可以共享访问它,但是闭包外部的代码无法看到它。


0
简单来说,第一个代码的问题在于每次调用它都会创建一个数组并从中返回一个值。这是因为你每次调用时都要创建一个数组,所以存在额外的开销。
在第二个代码中,它创建了一个闭包声明了一个仅包含一个数组的函数,并返回从该数组中返回值的函数。基本上,现在 digit_name 携带了自己的数组而不是在每次调用时创建一个新的。你的函数从闭包中的现有数组获取数据。

另一方面,如果没有正确使用,闭包可能会消耗内存。通常情况下,闭包用于保护内部代码免受外部作用域的影响,并且通常是通过限制外部访问来实现的。

除非所有对它们的引用都被“null”,否则对象不会被GC销毁。在闭包的情况下,如果您无法进入其中以清除这些内部引用,则对象将不会被GC销毁,并且永远会占用内存。


实际上,当闭包中涉及的所有函数超出范围时,所有相关对象都会被垃圾回收。这对于所有浏览器都是正确的。唯一的例外是在IE中包含循环引用的DOM对象。 - slebetman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接