《你不知道的JavaScript》这样描述:
当函数可以记住并访问所在的词法作用域时,就产生了闭包, 即使函数是在当前词法作用域之外执行
我的理解: 当一个函数在定义它的作用域以外的地方被调用时,它访问的依然是定义它时的作用域,这种现象称之为闭包。
闭包的本质是静态作用域。 因为javascript没有动态作用域, 所以函数访问的都是定义时的作用域,所以闭包才得以实现。
我们常见的闭包形式就是a函数套b函数,然后a函数返回b函数, 这样b函数在a函数以外的地方执行时, 依然能访问a函数的作用域。 其中“b函数在a函数以外的地方执行时”这一点, 才体现了闭包的真正的强大之处。
总之, 闭包只是基于静态作用域的一个变成技巧。
function a() {
var name = 'iceman';
function b() {
console.log(name);
}
return b;
}
var c = b();
c();
这样就清晰的展示了闭包:
- b 的词法作用域能访问 a 的作用域
- 将 b 当做一个返回值
- a 执行后, 将b 的引用赋值给 c
- 执行 c, 输出了变量 name
我们知道通过引用的关系, c就是b函数本身。执行c 能正常输出name, 这就是b能记住并访问它定义时所在的词法作用域, 而且 b 函数的运行还是在定义它时的词法作用域之外了。
正常来说, 当 a 函数执行完毕之后, 其作用域就会被销毁, 然后垃圾回收器会释放那段内存空间。 而闭包缺很神奇的将 a 的作用域存活下来, b 依然持有该作用域的引用, 这个引用就是闭包。
另: 对函数值的传递可以通过其他的方式, 并不一定值有返回该函数这一条路, 比如可以用回调函数:
function a() {
var name = 'iceman';
function b() {
console.log(name);
}
c ( b );
}
function c( fn ){
fn();
}
a();
本例中, 将内部函数 b 传递给 c, 当它在 c 中被运行时, 它同样是可以访问到name 变量的。
所以无论通过哪种方式将内部的函数传递到所在的词法作用域以外, 它都会持有对原始作用域的引用, 无论是何处执行这个函数都会使用闭包。
经典例子:
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
在这段代码中, 我们对其输出的预期是输出 1~10, 但却输出10次11. 这是因为setTimeout中的匿名函数执行的时候, for循环都已经结束了,for 循环结束的条件i 是大于10, 所以输出10次11。
究其原因: i 是声明在全局作用域中,定时器中的匿名函数也是执行在全局作用域中,所以 输出的是全局里的 i
原因知道了, 解决起来就简单了, 我们可以让 i 在每次迭代的时候, 都产生一个私有的作用域, 在这个私有作用域中保存当前i的值
for (var i = 1; i <= 10; i++) {
(function () {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})();
}
这样就达到我们的预期, 换一种比较优雅的写法改造,将每次迭代的 i 作为实参传递给自执行函数,自执行函数中用变量去接收:
for (var i = 1; i <= 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 1000);
})(i);
}
再看几个例子:
var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
// 局部变量num最后会保存在闭包中
var num = 42;
// 将一些对于函数的引用存储为全局变量
gLogNumber = function() { console.log(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5
var oldLog = gLogNumber;
setupSomeGlobals();
gLogNumber(); // 42
oldLog() // 5
这三个函数都具有对同一个局部变量num的访问权限。
**注意:在上述示例中, 当你再次调用setupSomeGlobals()时, 一个新的闭包(栈帧)就被创建了。 **
就变量 gLogNumber ,gIncreaseNumber,gSetNumber被有新闭包的函数覆盖。
在js中,如果在一个函数中声明了一个新的函数,那么当外部函数被调用时,内部函数会被重新创建
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// 使用j是为了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //输出 "item2 undefined" 3 次
在上面这个例子中, console会输出三次“item2 undefined”
为什么没有按照for循环的顺序,输出 item0, item1, item2的值呢?
这是因为在上述代码中, for 循环里产生了一个闭包, 我们分段试试执行这段代码:
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
console.log(fnlist)
// 使用j是为了防止搞混---可以使用i
// for (var j = 0; j < fnlist.length; j++) {
// fnlist[j]();
// }
}
可以看到fnlist 是一次赋值过程
result.push( function() {console.log(item + ' ' + list[i])} );
, 输出了一个包含三个匿名函数的数组可以看到每个匿名函数的[[Scopes]]包含的变量是去buildList里获取的,此时buildList里的i等于3, item的值为item2。
接下来我们执行下一段fnlistj;代码:
function testList() {
var fnlist = buildList([1,2,3]);
// 使用j是为了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
此时相当于执行 fnlist[0]();
fnlist[1]();
fnlist[2]();
分别执行数组的三个匿名函数,根据第一步的输出可得出,这三个匿名函数中的i已经值为3, item的值为item2(匿名函数中没有定义i和item, 就去上层执行环境中buildList去找)。实际上运行了三次{console.log('item2' + ' ' + list[3])} )
这行代码运行时, 由于传入函数buildList的数组为[1,2,3], list[3]的值类型自然是 undefined。
我们将传入的数组做一点修改, sonsole会将list[3]的值正确输出。 第一个for循环中的判断条件由 i < list.length 改为 i< 3, 传入的数组由[1,2,3]改为[1,2,3,4]
function buildList(list) {
var result = [];
for (var i = 0; i < 3; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1, 2, 3, 4]);
// 使用j是为了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //输出 "item2 4" 3 次
如果想输出 item0 1, item1 2,item2 3, 则可以改下代码
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
var fn = (function(item,i) {
return (item + ' ' + list[i])
})(item,i)
result.push( fn );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
console.log(fnlist)
}
testList()
思路是一样的,就是把 buildList 里的变量保存到自己的私有作用域当中。
参考:https://juejin.im/post/58832fe72f301e00697b672d
面试题:
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// 使用j是为了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //输出 "item2 undefined" 3 次