1. 变量的作用域
在讲闭包之前,首先来说一下JavaScript中变量的作用域问题。在JS中变量分为两种:局部变量和全局变量,与此相对应的两种变量的作用域分别为局部作用域和全局作用域。在函数内部定义的变量为局部变量,在函数外部定义的变量称作全局变量。JS特有的语法规定全局变量在函数内部和外部都可以使用,但是局部变量只能在在函数内部及其子函数中使用,我们来看如下案例:
var num = 18; //全局变量function func(){ var age = 22; //局部变量
console.log(age,num); //22 18}
func();
console.log(num,age); //18 age is not defined
结果分析:num为全局变量,age为局部变量,所以在函数内部访问age和num均可访问到。在函数外部可以访问全局变脸num,但是访问局部变量age时会报错,原因就是局部变量的作用域只在当前函数及其子函数中有效。
2. 如何在函数外部访问局部变量
针对JS变量作用域的特性,我们提出了如下疑问,如何在函数的外部访问局部变量?这时候我们可以考虑使用闭包,闭包实际上就是在函数内部再嵌套一个子函数将子函数作为返回值返回,接下来我们来看如下案例:
function func1(){ var age = 22; function func2(){ return age;
} return func2;
} var result = func1(); //得到的是函数func2这一整个函数
var _num = result(); //得到的是func2的执行结果也就是age的值
console.log(result); //function func2(){ return age; }
console.log(_num); //22
结果分析:首先要明白一个函数名称加不加括号的区别,比如说func1和func1(),func1表示的是这个函数本身,func1()表示的是这个名叫func1的函数的执行结果也就是它的返回值。其次我们来分析闭包的原理,因为func1是函数func2的子函数,所以在func2中可以访问到变量age,并将这个值作为函数func2的返回值返回,最后将整个func2函数作为func1函数的返回值。最后我们来分析一下result和_num的值,result是函数func1的执行结果,也就是func1的返回值func2函数本身,_num的值为func2的执行结果也就是变量age,至此,我们实际上已经将局部变量age的值获取到了并存在变量_num中。我们所提出的如何在函数外部访问局部变量的值的问题就已经解决了。
3. 什么是闭包
上一节代码中的func2函数实际上就是闭包。
闭包是指有权访问另一个函数作用域变量的函数,创建闭包的通常方式,是在一个函数内部创建另一个函数。
闭包的本质是一个函数,将函数内部和外部连接起来的桥梁。
4. 闭包的应用
闭包最大的用处有两个,一是可以在函数外部访问局部变量的值,第二个是变量的持久化,即让变量始终保存在内存中。我们来看如下实现变量累加功能的案例:
function func1(){ var age = 18; return ++age;
}
console.log(func1()); // 19
console.log(func1()); // 19
console.log(func1()); // 19
结果分析:我们想要实现变量累加的功能,但发现三次打印的结果都是19,原因是每次调用函数的时候,变量都会被初始化一次,调用完成之后变量会被释放,不会占据内存。应用我们刚才所学的闭包的知识来解决如下问题,代码如下:
function func1(){ var age = 18; function func2(){ return ++age;
} return func2;
} var result = func1();
console.log(result()); // 19
console.log(result()); // 20
console.log(result()); // 21
结果分析:在这段代码中,result实际上就是闭包函数,一共运行了三次,三次结果分别为19、20、21,这也就证明了函数func1中的局部变量age一直保存在内存中,并没有在func1调用后被自动清除。
为什么会这样呢?原因就在于func1是func2的父函数,而func2被赋给了一个全局变量result,这导致func2始终在内存中,而func2的存在依赖于func1,因此func1也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
案例二:
为了更好的理解闭包,我们来看另一个例子,下面有一组 li,点击 li 弹出对应的下标:
我们尝试采用如下代码去实现:
var aLi = document.querySelectorAll("ul li"); for(var i = 0; i < aLi.length; i++){
aLi[i].onclick = function(){
alert(i); // 5 }
}
结果分析:采用如上代码得出的结果是,点击每个li标签弹出来的都是 5 ,原因是function里面的内容只有当 li 发生点击事件的时候才会执行,当执行的时候for循环早就走完了,所以弹出来的 i 的值为5 。我们以前常见的解决方法是给每个 li 添加属性,代码如下:
var aLi = document.querySelectorAll("ul li"); for(var i = 0; i < aLi.length; i++){
aLi[i].index = i;
aLi[i].onclick = function(){
alert(this.index);
}
}
这是我们常用的一种方式,给每个 li 添加一个属性index ,将 i 的值存储在该属性中,然后去获取触发事件的当前对象的 index 属性值 。学完闭包之后我们尝试用闭包的知识去解决一下如上的问题:
var aLi = document.querySelectorAll("ul li"); for(var i = 0; i < aLi.length; i++){
aLi[i].onclick = (function(index){ return function(){
alert(index);
}
})(i)
}
以上代码涉及到匿名函数的自执行以及传值问题,此处不多加阐述。将点击事件后绑定的函数作为匿名函数,将 i 的值作为实参,index的值则为形参,利用如上解决方案,完美的实现了我们想要的效果。