这是我参与8月更文挑战的第2天,活动详情查看: 8月更文挑战
1. 什么是闭包
《JavaScript高级程序设计》中给出的定义:闭包是有权限访问其他函数作用域内的变量的一个函数。
《你不知道的JavaScript》中给出的定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
我的理解是,闭包就是能够读取其他函数内部变量的函数。
闭包原理:利用js中的垃圾回收机制,在回收被销毁的变量和函数时,如果发现被销毁的变量的函数正在被另一个函数使用,那么使用中的变量和函数将不被释放,长期驻留在内存中,直到整个程序退出时才被释放。
闭包的作用:一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz();
在上面这个例子中,bar()只是换了一种方法被调用,baz()输出2,此时bar()在它的词法作用域外面被执行了。
在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。
拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
释放闭包
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz();
baz = null //释放闭包
无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将 baz 分配给全局变量
}
function bar()
{
fn();
}
foo();
bar();
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
闭包在我们平常写代码的过程中很常见
回调函数
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
模块
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
2. 思考题
- 利用循环分别输出数字 1~5,每秒一次,每次一个。
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
这段代码在运行时会以每秒一次的频率输出五次 6。
首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是6。因此,输出显示的是循环结束时 i 的最终值。
延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 6 出来。
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
利用立即执行函数,形成一个封闭的作用域
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
这样也不行,i还是公用一个全局变量,IIFE 只是一个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。它需要有自己的变量,用来在每个迭代中储存 i 的值
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
2. 下面代码的输出结果是什么
function global(num){
return function(){
console.log(++num)
}
}
var demo = global(1);
demo()
demo()
demo()
使用++i(前置++),i先将自身的值自增1,再将自增后的值赋值给变量a
使用i++(后置++),i先将自身的值赋值给变量a,然后再自增1
3. 下面代码的输出结果是什么
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2));
console.log(add10(2));
4. 下面代码的输出结果是什么
function fun(n, o) {
console.log(o);
return {
fun: function(m) {
return fun(m, n);
}
};
}
// 1
var a = fun(0);
a.fun(1);
a.fun(2);
a.fun(3);
// 2
var b = fun(0).fun(1).fun(2).fun(3);
// 3
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);
答案
undefined,0,0,0
undefined,0,1,2
undefined,0,1,1