携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情
闭包是在前端面试中经常出现的题目,也是你在写前端代码中不可或缺的一项功能,那么闭包到底是什么呢,又需要在什么场景下利用呢?本文来讲一讲我对JS闭包的理解。
一个简单的自增函数
如果让你用JS写一个自增函数,每次调用时都会增加1并打印出来,你会怎么写?
最简单的,我们在函数外部定义一个变量,之后每次都修改这个变量:
let num = 0;
function add() {
num++;
console.log(num)
}
add();// 1
add();// 2
add();// 3
这样,每一次都可以增加1,但是,遇到一个问题,这个num是绑定在函数外部的,也就是Window对象上,那么我们只要在其他地方给num赋值,就会打乱自增函数的结果:
add(); //4
num = 0;
add(); //1
也就是说,我们不能将num交给全局,不能让她被随意访问,要把她保护起来。那么我们可不可以将其放到add函数中呢?这样外部就访问不到了:
function add() {
let num = 0;//注意这里一定要写let或var进行声明 否则会被处理为全局变量
num++;
console.log(num)
}
add();// 1
add();// 1
add();// 1
可以发现,保护虽然是保护了,但是num的值不变了。
变量的作用域
每个变量都在其声明的作用域内有效,一个作用域是指对象或函数的整个部分。比如最开始的写法中,num在全局作用域(也就是window)声明,因此在全局都可以访问,而第二个例子中num仅在add函数中声明,因此只在函数中生效,而每次调用函数后,这个变量都会被销毁,重新调用时生成了新的变量,因此num的值保持不变。
因此,要实现我们的功能需求,需要完成以下两点:
- 在函数的作用域中声明
num变量 - 使变量
num不被销毁
乍一看这两点有点冲突,似乎有点无厘头,但我们换一个思路,假设函数外部的代码访问以及处理了num,这个“违规”行为是不是就使得num保留下来了呢?
闭包的实现
说干就干,想让函数外部访问到num的最快方法就是声明一个全局方法:
function add() {
var num = 0;
window.addNum = function() {
num++;
console.log(num);
}
}
add(); // 记得先声明好再调用
window.addNum(); // 1
window.addNum(); // 2
window.addNum(); // 3
console.log(num); // Uncaught ReferenceError: num is not defined
这下就保证了num在外部访问不到,而且却一直不被销毁可以持续自增了。
然而,多声明了一个add函数是不是有点浪费,并且还有一不小心再调用一次add()使得num重置的风险。这时候我们可以采用立即调用函数表达式(IIFE)来帮忙:
如果有阅读过JQuery源码的话,可以看到JQuery也是一开始使用了立即调用函数表达式来解决命名空间与变量污染的问题
(function() {
var num = 0;
window.addNum = function() {
num++;
console.log(num);
}
})();
addNum();// 1
addNum();// 2
addNum();// 3
这样就做到了保证num自增的同时,又完全保护了num变量不被外界修改。
总结
- 闭包的用途:保证局部变量持续实现功能(不销毁)的同时不被污染(不暴露在外部)
- 闭包的方式:局部声明+外部调用
- 闭包的帮手:全局
window对象,立即调用函数表达式