关于闭包的一点理解
说闭包之前我们要先粗略的讨论一下作用域
每一行代码在运行时都会有一个作用域,在《你不知道的javascript》里是这样解释作用域的 "一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。 这套规则被称为作用域"。简单说就是里面存放着当前能访问到的变量,那这些变量是以什么形式储存呢,查找的时候又是如何查找的呢?js里有一个作用域链,当需要用到变量时,他会沿着作用域链一级一级的向下找,直到找到变量或者到达最外层(全局变量)才会停止,我们用代码来说明。
function foo(a) { console.log( a + b ); } var b = 2; foo( 2 ); // 4
当执行到var b = 2这一行时 作用域链中只包含了全局变量,当执行到foo函数内部时; 会先复制一份父级的作用域链,在向其前端推入当前的变量对象
b = undefined |
var b = 2时的作用域链
a = 2 |
b = 2 |
执行到foo函数内部时的作用域链
我们再来讨论一下js的块级作用域
js中函数可以产生作用域,当我们需要作用域时,不想新建全局变量污染环境是,我们就可以创建一个函数并执行他,而我们执行函数时往往通过需要调用函数名称,如果我们只是向单纯的创建一个作用域,函数名字函数名字也会污染环境,这时我们就需要立即执行函数来帮助我们,即创建一个匿名函数并立即调用他,看例子
//普通函数 function fn(a){ console.log(a) } fn(2) //立即执行函数 (function (a){ console.log(a) })(2)
由于function(){}()这样的写法会产生错误,所以立即执行函数需要用()将匿名函数包起来,(function(){})()这样就是立即执行函数的样子,和普通函数一样也可以进行传参
下面我们进入主题,说一下闭包
闭包可以使我们跨作用域访问变量,其中的一种表现就是在函数外部可以访问到属于函数内部的变量,我们来看一个例子
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2
这就是一个最简单的闭包,本来变量a时函数foo的内部变量,正常情况下时无法访问的但是我们可以在其内部再写一个函数,获得变量a再将这个内部函数返回,即可获得这个内部变量,当然这只是为了演示闭包,这个函数没有实际意义。
我们再来看一下闭包的其他表现形式,最经典的for循环
function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push(function () { console.log(i) ; });
} return arr; } var results = count(); var f1 = results[0]; var f2 = results[1]; var f3 = results[2];
f1();
f2();
f3();
count函数会返回一个数组,数组中有三个元素,分别是三个函数,根据期望f1(), f2(), f3(), 应该分别打印 1,2,3,但是结果却是打印的三个4
我们来分析一下,在for循环里向数组push了匿名函数,而这个匿名函数中使用了i这个外部变量,也就是说在匿名函数的作用域链中有一个变量i,当for循环执行完毕之后i变成了4,当我们调用f1()时也就是调用了匿名函数,此时函数要去查找i,找到作用域链中的i,发现此时i是4,同理f2(), f3()一样都会打印4。
为了印证匿名函数中作用域链的i是随着for循环在变化的,我们可以这样
function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push(function () { console.log(i) ; }); arr[0]() } return arr; } count() //1 2 3
通过上面我们就可以清晰的看出匿名函数的i在for循环的过程中是变量,匿名函数被调用时会去作用域链中查找这个变量,for循环未结束之前循环一次i会加一。
但是这往往不是我们想要的结果,我们想要把这个i保存下来,不随着for循环的改变而改变,让这三个匿名函数作用域链中的i不再指向for循环中的i,我们可以创建一个块级作用域,将i保存下来,我么就可以用上面介绍的立即执行函数来改造一下
function count() { var arr = []; for (var i=1; i<=3; i++) { (function(i){ arr.push(function () { console.log(i) ; }); })(i) } return arr; } var results = count(); var f1 = results[0]; var f2 = results[1]; var f3 = results[2]; f1(); f2(); f3();
这样每次for循环的过程中,都会产生一个新的立即执行函数,而每个立即执行函数产生一个新的作用域,来保存不同的i,当执行arr中的匿名函数的时候就会找到已经保存下来的i(因为函数的传值的按值传递的,所以可以将i保存下来)。
上面的方法是传统的方法,现在解决作用域我们还有一个新型武器 let ,let声明的变量会自动产生作用域(let的其他特性在这不做介绍),现在我们可以用let来达到相同的效果,为arr中的每一个匿名函数创建一个新的作用域
function count() { var arr = []; for (let i=1; i<=3; i++) { arr.push(function () { console.log(i) ; }); } return arr; } var results = count(); var f1 = results[0]; var f2 = results[1]; var f3 = results[2]; f1(); f2(); f3();
console.log(123)