前言
对于我来说,初次接触到闭包时我是懵逼的,因为那是我第一次看到函数嵌套,也是那时,我不禁怀疑Java的万物皆对象是个假命题。好吧,有点扯远了。接着回归正题,请带着这几个问题看这篇文章:
1.什么是闭包?
2.什么情况会产生闭包?
3.使用闭包应该注意什么?
4.闭包带来了什么好处?
作用域
要想理解闭包,必须要理解作用域。JavaScript的作用域可以看成三种:全局作用域、函数作用域、块级作用域。在这里只需要用到两种:全局作用域和函数作用域,如果对块级作用域感兴趣,可以看看我以前写的一篇文章带你弄懂var、let与const。
例1:
function func1(){
var _name = 'func1';//因为window下面有个name属性 所以所有的变量用_name命名
console.log('_name : ' + _name );//_name : func1
}
function func2(){
var _name = 'func2';
console.log('_name : ' + _name );//_name : func2
}
func1();
func2();
对于上面代码中的两个function中的变量name来说,它们是不会互相干涉的,因为它们都各自生活在不同的函数作用域。在func1中是不能访问到func2中的变量,而func2中也不能访问到func1中的变量。
例2:
function outside(){
var _name = 'outside';
function inside(){
var _name = 'inside';
console.log('_name : ' + _name );//_name : inside
}
inside();
}
outside();
例3:
function outside(){
var _name = 'outside';
function inside(){
console.log('_name : ' + _name );//_name : outside
}
inside();
}
outside();
这里的两套代码都是inside方法被嵌套在outside中,唯一的不同是inside中变量name的有无。在例2中inside方法中输出的name是inside方法中声明的变量name的值,而在例3中,由于inside方法中未对变量name进行声明,所以输出的是outside中声明的变量name的值。
例4:
var _name = 'window';
function outside(){
function inside(){
console.log('_name : ' + _name );//_name : window
}
inside();
}
outside();
在例4中,情况又与先前不一致,outside和inside中都未对变量name进行声明,而是在window(假设现在运行的环境在浏览器)下对name进行了声明。
例5:
function outside(){
function inside(){
console.log('_name : ' + _name );//_name is not defined
}
inside();
}
outside();
纵观例1、2、3、4、5输出的变量name的值,我们可以得出结论:
函数作用域中的值互不影响;当函数发生嵌套时,内部函数首先访问自身作用域中的变量,如果没有此变量,则继续访问立自己最近的外部函数的作用域,如果找到,则使用此变量,如果还是没有此变量,则继续访问…一直到全局作用域,如果找到则使用,如果还是没有,就报 is not defined。
closure
这里直接借用例3的代码
function outside(){
var _name = 'outside';
function inside(){
debugger
console.log('_name : ' + _name );//_name : outside
}
inside();
}
outside();
由于在inside作用域中没有声明变量_name,在inside被执行时会去执行环境(也就是outside中)寻找变量_name;在outside中是声明了变量_name,inside就直接使用这个在outside中声明的变量_name,从而在它(这里指的是inside)的Scope(作用域)中生成了Closure(闭包)。
使用场景
学过Java的小伙伴肯定知道,在变量声明前加个private,这个变量就变成私有变量了,但是JavaScript没有private,那在JavaScript中是不是就不能创建私有变量了呢?答案肯定是否定的。其实在我以前写的 函数防抖和函数节流这两篇文章中就有使用闭包创建私有变量的案例,感兴趣的可以看一下。
//闭包创建私有变量
function timerFunc(){
console.log("移动时间:"+new Date().getTime());
}
function moveFunc(func,delay){
var timer = null;
return () => {
if(null != timer){
clearTimeout(timer);
}
timer = setTimeout(()=>{
func.apply(this, arguments)
} ,delay);
}
}
window.addEventListener("mousemove", moveFunc(timerFunc,1000));
//闭包创建私有变量写法
function ableClick(func,delay){
var time = null;
return () => {
var nowTime = new Date().getTime();
if(time == null || (nowTime - time > delay)){
time = nowTime;
func.apply(this,arguments);
}
}
}
function clickFunc(){
console.log("点击时间:"+new Date().getTime());
}
window.addEventListener("click", ableClick(clickFunc,1000));
这里其实只要理解了函数声明的函数也是一个对象,应该也就没什么难点了。
闭包还有一个特别有意思的使用场景,我在 一篇文章带你弄懂var、let与const中有提到过,像下面的代码:
var arr = [];
for(let i = 0,length = 10;i<length;i++){
arr.push((() => {
return i;
}));
}
arr[1]();//1
babel为了兼容浏览器,在进行代码以及语法的转换时,是会把像上述代码中for循环中的let替换成var的,而为了模拟let带来的块级作用域的效果,同时还会使用上闭包:
function outside(num){
function inside(){
return num;
}
return inside;
}
var arr = [];
for(var i = 0,length = 10;i<length;i++){
arr.push(outside(i));
}
arr[1]();//1
注意事项
下面的代码是将例3进行修改后得到的。
//第一步
function outside(){
var _name = 'outside';
function inside(){
debugger
console.log('_name : ' + _name );//_name : outside
}
return inside;
}
var func = outside();
//第二步
func();
第一步,我们先得到outside函数返回的inside对象,并将func变量指向它;
第二步,执行func方法;
可能你已经发现,outside方法都已经return了,在inside中还能使用outside的变量。这里按照我接触的知识,v8肯定是做了大量的优化了,但是这里具体是优化到持有outside不释放呢,还是只是持有_name不释放,我也不是很清楚。如果只是持有_name不释放,我个人觉得问题不大,如果是持有outside不释放,而outside中又有占据大内存的对象,肯定就容易引起内存泄漏了。不过我试过在outside中再声明一个变量,如果在inside中不使用此变量,是在Closure中是不会有此变量的(只在v8下面试过)。
总结
我个人认为理解闭包最重要的两个点在于函数作用域和作用域链,一定要从这个方向去刨析它的本质,摸清它的特性,探寻它的客观规律以及存在的条件。同时也应该牢牢记住,它是一把双刃剑,切记不可滥用,避免造成内存溢出或泄露。