目录
闲言小叙
相信能学习到闭包,说明你一定是一位基础已经很不错的前端工程师了,但学习的路途漫漫,学海无涯,相信看完这篇文章,你会有更多收获。
一、学习闭包前的准备工作
1、在学习闭包之前我们需要先了解 作用域、作用域继承、垃圾回收机制
二、闭包的概念
简单来说:声明在一个函数中的函数,叫做闭包函数。内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回。
三、闭包的特点
1、让外部访问函数内部变量成为可能,可以让函数内部的变量超出它本身的作用域在外部使用。
2、闭包可以隔离作用域,不暴露私有成员的目的,减少了变量冲突的风险;
3、闭包同样存在缺点:局部变量会常驻在内存中,严重情况下会造成内存泄漏;
四、常见的闭包函数
1、直接return内部函数
function f1() {
var a = 100;
function f2() {
console.log(a);
}
return f2;
}
var foo = f1();
foo(); //100
> 在函数f1中return出了函数f2,但在原有的作用域链中,f2中的a变量依然指向f1中声明的a变量,所以在return出函数f2后,原有的作用域链并没有得到释放,所以f2中的a变量访问的依然是f1中的a,因为这种情况下,垃圾回收机制判断a还处于引用状态,无法将其回收,这也是使用过多闭包会造成内存泄漏的原因。
2、函数作为参数
var a = 1;
function f1(){
var a = 2;
function f2(){
console.log(a);
}
return f2
}
function f3(p){
var a = 3;
p();
}
f3(f1());//2
> 函数f3(f1()),可以先解析为函数f1执行,因为函数f1执行会返回f2函数体,所以函数执行为f3(f2),f2在f3中执行,但因其是在f1中返回出的函数,所以其原本具有的作用域链依然存在,所以进行变量查找的时候,依然采用之前的作用域链进行查找。
3、自执行函数
var a = 100;
(function f1(){
console.log(a); // 100
})()
> 立即执行函数执行,访问的上层作用域为window,所以访问到了全局中的a变量。
4、返回函数执行
function f1(){
var i = 0;
function f2(){
i++;
console.log(i);
}
return f2;
}
var inner = f1();
inner();
inner();
inner();
var inner2 = f1();
inner2();
inner2();
inner2();
//1 2 3 1 2 3
> 因为每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址,所以就导致i的值会一直增加,而当var inner2 = f1();此时重新将f1执行的返回值f2函数赋值给inner2,所以打印出的i的值又会回到初始值。
5、多层函数的嵌套
var add = function(x) {
var sum = 1;
var tmp = function(x) {
sum = sum + x;
return tmp;
}
tmp.toString = function() {
return sum;
}
return tmp;
}
alert(add(1)(2)(3)); //6
> 函数一层层执行,但原有的作用域链不会被断开,依然会采取原有的作用域链进行变量查找。
6、事件处理函数中闭包的用法
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
var lis = document.querySelectorAll('li');
// 循环给每个li添加事件
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function(){
lis[i].style.color = 'red'; // TypeError: Cannot read properties of undefined (reading 'style')
console.log(lis[i]); // undefined
}
}
> 不可用此方法为事件循环添加事件,因为所有事件函数都是异步执行,而且var会有变量提升,当事件绑定完毕后,再执行时点击事件时,此时所有的i都为5,lis数组中根本不存在索引为5的元素,所以访问出就是undefined,事件自然会报错。
解决方案一:使用闭包函数
for (var i = 0; i < lis.length; i++) {
(function(i){
lis[i].onclick = function(){
lis[i].style.color = 'red'
}
})(i)
}
> 立即执行函数为每个事件单独创建了一个块级作用域,使得每个事件所使用的i都是立即执行函数传入的i,所以相互之间互不干扰。
解决方案二:使用块级声明方式
for (let i = 0; i < lis.length; i++) {
lis[i].onclick = function(){
lis[i].style.color = 'red'
}
}
> const 和 let 都创建出了块级的作用域,其本质和闭包函数一样创建出互相独立的块级作用域,使事件绑定不再冲突
7、函数柯里化
function add(x){
return function(y){
return function(z){
return x+y+z
}
}
}
console.log(add(1)(2)(3)) // 6
五、闭包面试题
关于闭包有一个比较经典的面试题,当面试官问你在平常怎么使用闭包的时候,千万不要觉得自己学会了闭包想跟面试官炫耀一下技术水平就说“这玩意会让内存泄漏,我平常都不用”,如果这样说了,只能说明你对闭包的理解还不到家,可以简单的说“let const 模块化代替闭包解决变量污染问题。 但是:模块化编译后都是闭包语法。在 react 开发中,高阶组件,统一管理状态就是闭包。在 react 中高阶函数存在形式基本都是闭包”。这种是简单的说法和更证明许多语法编译后都是闭包就放在后续再更新了。