理解Javascript中的闭包

时间:2010-10-11 01:49:01

标签: javascript closures

我正试图用Javascript来解决问题。

以下是教程中的示例:

function greeter(name, age) {
  var message = name + ", who is " + age + " years old, says hi!";

  return function greet() {
    console.log(message);
  };
}

// Generate the closure
var bobGreeter = greeter("Bob", 47);

// Use the closure
bobGreeter();

作者说这是使用闭包来制作私有变量的有效方法,但我不明白这一点。

有人可以像这样启发编码的好处吗?

8 个答案:

答案 0 :(得分:23)

闭包是一对 a function 环境,在其中定义了它(假设lexical scoping,哪个JavaScript使用)。因此,闭包函数可以访问其环境中的变量;如果没有其他函数可以访问该环境,那么其中的所有变量都是私有的,只能通过闭包函数访问。

您提供的示例可以很好地证明这一点。我添加了内联注释来解释环境。

// Outside, we begin in the global environment.
function greeter(name, age) {
  // When greeter is *invoked* and we're running the code here, a new
  // environment is created. Within this environment, the function's arguments
  // are bound to the variables `name' and `age'.

  // Within this environment, another new variable called `message' is created.
  var message = name + ", who is " + age + " years old, says hi!";

  // Within the same environment (the one we're currently executing in), a
  // function is defined, which creates a new closure that references this
  // environment. Thus, this function can access the variables `message', `name',
  // and `age' within this environment, as well as all variables within any
  // parent environments (which is just the global environment in this example).
  return function greet() { console.log(message); };
}

运行var bobGreeter = greeter("Bob", 47);时,会创建一个新的闭包;也就是说,您现在已经有了一个新的函数实例以及创建它的环境。因此,您的新函数在所述环境中引用了“message”变量,尽管没有其他人这样做。

额外阅读:SICP Ch 3.2。虽然它侧重于Scheme,但这些想法是相同的。如果您对本章有所了解,那么您将对环境和词法范围的工作方式有一个良好的基础。

Mozilla还有一个专门针对explaining closures的页面。

答案 1 :(得分:8)

闭包的目的是使你在给定函数中使用的变量保证“闭合”,这意味着它们不依赖于外部变量 - 它们只依赖于并使用它们的参数。这使得您的Javascript方法更接近pure function,即为相同的给定参数返回相同值的方法。

不使用封口,您的功能就像瑞士奶酪,它们会有洞。闭包会堵塞这些洞,因此该方法不依赖于范围链中较高的变量。

现在,到目前为止,我的回答仅仅是组织代码和样式。举个简单的例子吧。在注释的行中,我调用一个函数,并捕获变量a的值以供将来使用。

var a = "before";
var f = function(value) {
    return function()
    {
      alert(value);
    }
} (a); //here I am creating a closure, which makes my inner function no longer depend on this global variable
a = "after";

f(); //prints "before"

现在,你为什么需要这样做?嗯,这是一个实际的例子。请考虑以下使用jQuery添加5个文档链接的代码。当您单击某个链接时,您会希望它与该链接关联的数字为alert,因此单击您认为的第一个会提醒0,依此类推。 但是,情况并非如此,每个链接都会alert的值为5.这是因为我定义的函数取决于变量i,该变量在功能的上下文。我传入bind的功能是瑞士奶酪功能。

for (var i = 0; i < 5; i++)
{
    var a = $('<a>test link</a>').bind('click', function(){
        alert(i);
    });
    $(a).appendTo('body');
}

现在,让我们通过创建一个闭包来解决这个问题,这样每个链接都会alert正确的数字。

for (var i = 0; i < 5; i++)
{
    var fn = function (value) {
        return function() {
            alert(value);
        };
    } (i); //boom, closure
    var a = $('<a>test link</a>').bind('click', fn);
    $(a).appendTo('body');
}

答案 2 :(得分:4)

我不认为这是私有变量的一个很好的例子,因为没有真正的变量。闭包部分是函数greet可以看到message(外部不可见,因此私有),但它(或其他任何人)没有改变它,所以它更像是一个恒定。

以下示例怎么样?

function make_counter(){
    var i =0;
    return function(){
        return ++i;
    }
}

var a = make_counter();
console.log(a());  // 1
console.log(a());  // 2
var b = make_counter();
console.log(b());  // 1
console.log(a());  // 3

答案 3 :(得分:2)

更好的例子可能是

function add(start, increment) {
  return function() {
      return start += increment;
  }
}

var add1 = add(10, 1);

alert(add1()); // 11
alert(add1()); // 12

这里,每次调用返回的函数时,都会添加1.内部是封装的。

返回的函数仍然可以访问其父变量(在本例中为startincrement)。

在较低层次的思考中,我认为这意味着函数的堆栈在返回时不会被销毁。

答案 4 :(得分:2)

一旦你“得到它”,你会想知道为什么花了这么长时间来理解它。这就是我感觉的方式。

我认为Javascript中的函数范围可以用相当简洁的方式表达。

  

函数体可以访问在函数声明的词法环境中可见的任何变量,也可以访问通过函数调用创建的任何变量 - 即,在本地声明的任何变量,作为参数传递或以其他方式提供按语言(例如thisarguments)。

答案 5 :(得分:2)

它被称为“闭包”,因为它们围绕自由变量“关闭”,并且有更多的方法可以使用它然后只隐藏状态。例如,在闭包来自的函数式编程中,它们通常用于减少参数数量或为函数设置一些常量。假设您需要函数goodEnough()来测试某些结果是否比某个阈值更好。您可以使用2个变量的函数 - 结果和阈值。但是你也可以“封闭”你的常数内部函数:

function makeThresholdFunction(threshold) {
    return function(param) {
        return (param > threshold);
    }

}

var goodEnough = makeThresholdFunction(0.5);
...
if (goodEnough(calculatedPrecision)) {
   ...
}

使用闭包你也可以使用所有技巧,例如它们的组成:

function compose(f1, f2) {
    return function(arg) {
        return f1(f2(arg));
    }
}

var squareIncremented = compose(square, inc);
squareIncremented(5); // 36

有关封闭设计和使用的更多信息,请访问SICP

答案 6 :(得分:1)

我发现这篇文章非常有用。

When is a function not a function?

答案 7 :(得分:1)

//Lets start with a basic Javascript snippet
function generateCash() {
    var denomination = [];
    for (var i = 10; i < 40; i += 10) {
        denomination.push(i);
    }
    return denomination;
}

这是Javascript中的一个基本函数语句,它返回一个[10,20,30]

的数组
//--Lets go  a step further

function generateCash() {
    var denomination = [];
    for (var i = 10; i < 40; i += 10) {
        denomination.push(console.log(i));
    }
    return denomination;
}

当循环迭代时,这将打印10,20,30顺序,但将返回[未定义,未定义,未定义]的数组,主要原因是我们没有推动i的实际值,我们只是打印它out,因此在每次迭代时,javascript引擎都会将其设置为undefined。

//--Lets dive into closures

function generateCash() {
    var denomination = [];
    for (var i = 10; i < 40; i += 10) {
        denomination.push(function() {
            console.log(i)
        });
    }
    return denomination;
}

var dn = generateCash();
console.log(dn[0]());
console.log(dn[1]());
console.log(dn[2]());

这有点棘手,你期望输出是什么,它会是[10,20,30]?答案是否定的,让我们看看这是怎么回事。首先,在创建dn时创建全局执行上下文,我们还有generatecash()函数。现在我们看到,当for循环迭代时,它会创建三个匿名函数对象,可能很容易认为push函数中的console.log也被触发了,但实际上并非如此。我们已经调用了generateCash(),因此push函数只是创建了三个匿名函数对象,它不会触发函数。在迭代结束时,当前本地上下文从执行堆栈弹出,它离开状态i:40和arr:[functionobj0(),functionob1(),functionobj2()]。

因此,当我们开始执行最后三个语句时,它们都输出40,因为它无法从当前范围获取i的值,它会向上移动范围链并发现i的值已经所有这些都将触发40的原因是由于dn的每个组件都位于相同的执行上下文中,并且所有这些组件都无法在当前范围内找到i的值,因此将在范围链上运行找到我设置为40并分别输出