通过示例学习JavaScript闭包

译者按: 上一篇博客,我们通过实现一个计数器,了解了如何使用闭包(Closure),这篇博客将提供一些代码示例,帮助大家理解闭包。

原文: JavaScript Closures for Dummies

译者: Fundebug

本文采用意译,版权归原作者所有

闭包并不神奇

其实,只要你领会了闭包的关键概念,一切就非常简单了。作为 JavaScript 开发者,你应该可以理解以下代码:

Example 1

function sayHello(name) {
var text = "Hello " + name;

var sayAlert = function() {
console.log(text);
};

sayAlert();
}

sayHello("Bob"); // 输出"Hello Bob"

sayHello()函数中定义并调用了sayAlert()函数;sayAlert()作为内层函数,可以访问外层函数 sayHello()中的text变量。理解这一点,你就可以继续阅读这篇博客了。

一个闭包示例

两句话总结闭包(注意,这个定义并不规范,但是有助于理解):

  • 闭包就是函数的局部变量,这些变量在函数 return 之后仍然可以访问
  • 闭包就是函数的内存堆栈,这个内存堆栈在函数 return 之后并没有被收回

Example 2

function sayHello2(name) {
var text = "Hello " + name; // 局部变量

var sayAlert = function() {
console.log(text);
};

return sayAlert;
}

var say2 = sayHello2("Jane");
say2(); // 输出"Hello Jane"

调用sayHello2()函数返回了sayAlert,它是一个引用变量,指向一个函数。相信大多数 JavaScript 程序员能够理解什么是引用变量,而 C 程序员则可以把sayAlert以及say2理解为指向函数的指针。

C 指针与 JavaScript 引用变量并无实质区分。在 JavaScript 中,不妨这样理解,指向函数的引用变量不仅指向函数本身,还隐含地指向了一个闭包。

代码中匿名函数function() { alert(text); }是在另一个函数,即sayHello2()中定义的。在 JavaScript 中,如果你在函数中定义了一个函数,则创建了闭包。

对于 C 语言,以及其他绝大多数语言:函数 return 之后,其局部变量将无法访问,因为内存中的堆栈会被销毁。

对于 JavaScript,如果你在函数中定义函数的话,当外层函数 return 之后,其局部变量仍然可以访问。代码中已经证明了这一点:当sayHello2()函数 return 之后,我们调用了say2()函数,成功打印了text变量,而text变量正是sayHello2()函数的局部变量。

更多示例

如果只是从定义的角度去理解闭包,显然是非常困难。然而,如果通过代码示例去理解闭包,则简单很多。因此,强烈建议你认真地理解每一个示例,弄清楚它们是如何运行的,这样你会避免很多奇怪的 BUG。

Example 3

Example 3中,say667()函数 return 后,num变量将仍然保留在内存中。并且,sayNumba函数中的num变量并非复制而是引用,因此它输出的是667而非666

function say667() {
var num = 666; // say667()函数return后,num变量将仍然保留在内存中

var sayAlert = function() {
console.log(num);
};

num++;

return sayAlert;
}

var sayNumba = say667();

sayNumba(); // 输出667

Example 4

Example 4中,3 个全局函数gAlertNumber,gIncreaseNumber,gSetNumber指向了同一个闭包,因为它们是在同一次setupSomeGlobals()调用中声明的。它们所指向的闭包就是setupSomeGlobals()函数的局部变量,包括了num变量。也就是说,它们操作的是同一个num变量。

function setupSomeGlobals() {
var num = 666;

gAlertNumber = function() {
console.log(num);
};

gIncreaseNumber = function() {
num++;
};

gSetNumber = function(x) {
num = x;
};
}

setupSomeGlobals();
gAlertNumber(); // 输出666

gIncreaseNumber();
gAlertNumber(); // 输出667

gSetNumber(5);
gAlertNumber(); // 输出5

Example 5

Example 5的代码比较难,不少人都会犯同样的错误,因为它的执行结果很可能违背了你的直觉。

function buildList(list) {
var result = [];

for (var i = 0; i < list.length; i++) {
var item = "item" + list[i];
result.push(function() {
console.log(item + " " + list[i]);
});
}

return result;
}

var fnlist = buildList([1, 2, 3]);

for (var j = 0; j < fnlist.length; j++) {
fnlist[j](); // 连续输出3个"item3 undefined"
}

result.push( function() {alert(item + ‘ ‘ + list[i])}将指向匿名函数function() {alert(item + ‘ ‘ + list[i])}的引用变量加入了数组,其效果等价于:

pointer = function() {
alert(item + " " + list[i]);
};
result.push(pointer);

代码执行后,连续输出了 3 个”item3 undefined”,明显与直觉不同。

调用buildList()函数之后,我们得到了一个数组,数组中有 3 个函数,而这 3 个函数指向了同一个闭包。而闭包中的item变量值为“item3”i变量值为3。如果理解了 3 个函数指向的是同一个闭包,则输出结果就不难理解了。

Example 6

Example 6中,alice变量在sayAlert函数之后定义,这并未影响代码执行。因为返回函数sayAlice2所指向的闭包会包含sayAlice()函数中的所有局部变量,这自然包括了alice变量,因此可以正常打印”Hello Alice”。

function sayAlice() {
var sayAlert = function() {
console.log(alice);
};

var alice = "Hello Alice";

return sayAlert;
}

var sayAlice2 = sayAlice();

sayAlice2(); // 输出"Hello Alice"

Example 7

Example 7可知,每次调用newClosure()都会创建独立的闭包,它们的局部变量numref的值并不相同。

function newClosure(someNum, someRef) {
var anArray = [1, 2, 3];
var num = someNum;
var ref = someRef;

return function(x) {
num += x;

anArray.push(num);

console.log(
"num: " +
num +
"; " +
"anArray " +
anArray.toString() +
"; " +
"ref.someVar " +
ref.someVar
);
};
}

closure1 = newClosure(40, { someVar: "closure 1" });
closure2 = newClosure(1000, { someVar: "closure 2" });

closure1(5); // 打印"num: 45; anArray 1,2,3,45; ref.someVar closure 1"
closure2(-10); // 打印"num: 990; anArray 1,2,3,990; ref.someVar closure 2"

总结

严格来讲,我对闭包的解释并不准确。不过,将闭包简单地看做局部变量,理解起来会更加简单。

参考链接

关于Fundebug

Fundebug专注于JavaScript、微信小程序、支付宝小程序线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了80亿+错误事件。欢迎大家免费试用

版权声明

转载时请注明作者 Fundebug以及本文地址:
https://blog.fundebug.com/2017/08/07/javascript-closure-examples/

您的用户遇到BUG了吗?

体验Demo 免费使用