闭包
1. 什么是闭包?
作为一名前端无知小白,我猜MDN的中文版一定是机器翻译的,因为有时翻到上面的中文怎么看都不像是人话,比如闭包的定义:
Closures (闭包)是使用被作用域封闭的变量,函数,闭包等执行的一个函数的作用域。通常我们用和其相应的函数来指代这些作用域。(可以访问独立数据的函数)
能看得懂这个定义才真有鬼了。
好的,我们还是用蹩脚的英文来看看吧:
“A closure is the combination of a function and the lexical environment within which that function was declared.“
闭包是函数以及在函数声明下的词法环境的结合。
函数?变量?词法环境?
似乎感觉懂了一点点?
翻了下JS权威指南,里面说”Javascript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为闭包“。
所以我们从变量的作用域开始谈起。
2. 变量作用域
先来说几个概念:
全局变量:在所有作用域都可访问的变量,在函数外定义的变量就是全局变量
局部变量:在函数中使用关键字声明的变量,它的作用域只在声明该变量的函数内,在函数外面是访问不到该变量的。
词法作用域:词法作用域也叫静态作用域,也就是说函数的作用域在函数定义的时候就决定了,而不是调用的时候决定。JavaScript采用静态作用域,变量的作用域完全由写代码期间函数声明的位置来定义的。
话不多说,上代码:
代码1:
var func = function(){
var a = 'closure'
console.log(a); // closure
}
func();
console.log(a); // Uncaught ReferenceError: a is not defined
局部变量a只能在函数内部使用,函数调用结束时,该变量就会被垃圾回收机制回收而销毁
代码2:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
3. 变量的生命周期
我们知道的事情有:
1. 一个局部变量当定义该变量的函数调用结束时,该变量就会被垃圾回收机制回收而销毁。再次调用该函数时又会重新定义了一个新变量。
2. 按照代码书写时的样子,内部函数可以访问函数外面的变量。
那么,如果在函数内部声明一个内部函数,并将内部函数作为值返回,调用外部函数之后,内部函数保持对外部函数词法作用域的引用,这样会发生什么呢?由于内部函数作为值返回了出去,所以外层函数执行完毕,其词法作用域中的变量也不会被销毁。
这就是闭包!
老规矩,上代码:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
这段代码非常清晰地展示了闭包,函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作一个值类型进行传递。在 foo() 执行后,其返回值赋值给变量 baz 并调用 baz(),也就是调用了内部函数bar()。bar()在自己定义的词法作用域以外的地方执行。由于bar()保持着队foo()词法作用域的引用,所以foo()在执行完后内部作用域也不会被程序销毁。
循环和闭包
for循环是常见的说明闭包的例子,也是我这样的小白非常容易错的地方。
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
我们可能会简单的以为控制台会打印出‘ 0 1 2 3 4 ’,可事实却打印出了5个‘ 5 ’,这又是为什么呢?我们发现,setTimeout 函数时异步的,等到函数执行时,for循环已经结束了,此时的 i 的值为 5,所以 function() { console.log(i) } 去找变量 i,只能拿到 5。
所以,改进一下:
for (var i = 0; i < 5; i++) {
!function (i) {
setTimeout(function () {
console.log(i)
}, 0)}(i)
}
我们套用了一个立即执行函数,当i=0, 此时 function(i){} 此匿名函数中的 i 的值为 0,等到 setTimeout 执行时顺着外层去找 i,这时就能拿到 0。如此循环,就能拿到想要的 0 1 2 3 4。
作为小白,可能还是太明白什么是立即执行函数,下篇我们会详细介绍。这里我们先换个例子。
function constfunc(v){return function(){ return v; };} // 这个函数返回一个总是返回v的函数
var funcs = []; // 创建一个空的数组
for(var i=0; i<10;i++){funcs[i] = constfunc(i)}
for(i=0;i<funcs.length;i++) {console.log(funcs[i]())}
这样就可以打印出0~9了。
不知道你明白了没有,反正我是明白啦~
今天给自己加个鸡蛋!