一篇文章带你弄懂闭包

前言

对于我来说,初次接触到闭包时我是懵逼的,因为那是我第一次看到函数嵌套,也是那时,我不禁怀疑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下面试过)。

总结

我个人认为理解闭包最重要的两个点在于函数作用域和作用域链,一定要从这个方向去刨析它的本质,摸清它的特性,探寻它的客观规律以及存在的条件。同时也应该牢牢记住,它是一把双刃剑,切记不可滥用,避免造成内存溢出或泄露。

猜你喜欢

转载自blog.csdn.net/qq_35508835/article/details/106041264
今日推荐