js 闭包
wyx 4/3/2023 js
# 闭包
闭包其实就是一个可以访问其他函数内部变量的函数。创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以 访问到当前函数的局部变量。
闭包产生的本质就是:当前环境中存在指向父级作用域的引用
# 闭包的特性
- 函数内再嵌套函数
- 内部函数可以引用外层的参数和变量
- 参数和变量不会被垃圾回收机制回收
# 闭包的用途
- 创建私有变量 闭包可以使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量
- 使已经运行结束的函数上下文中的变量对象继续留在内存中 因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收
其实闭包的本质就是作用域链的一个特殊的应用。
原始类型存储位置:局部变量才是被存储在栈上,全局变量存在静态区域上,其它都存储在堆上。
# 闭包产生的原因
闭包产生的本质就是:当前环境中存在指向父级作用域的引用
# 闭包的表现形式
- 返回一个函数
- 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
// 定时器
setTimeout(function handler(){
console.log('1');
},1000);
// 事件监听
$('#app').click(function(){
console.log('Event Listener');
});
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 作为函数参数传递的形式
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
foo(); // 输出2,而不是1
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- IIFE(立即执行函数)(匿名函数)
var a = 2;
(function IIFE(){
console.log(a); // 输出2
})();
1
2
3
4
2
3
4
# 高频面试题
# 1、说说你对闭包的理解
- 使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,因为只有函数才会产生作用域的概念
- 闭包最大的用处
- 可以读取函数内部的变量
- 让这些变量始终保持在内存中
- 另一个用处:封装对象的私有属性和私有方法
var aaa = (function () {
var a = 1;
function bbb() {
a++;
console.log(a);
}
function ccc() {
a++;
console.log(a);
}
return {
b: bbb, //json结构
c: ccc,
};
})();
console.log(aaa.a); //undefined
aaa.b(); //2
aaa.c(); //3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 好处:能够实现封装和缓存
- 坏处:消耗内存,不正当使用会造成内存泄漏,滥用闭包会造成网页性能问题(解决办法:在退出函数之前,将不使用的局部变量全部删除)
- 应用场景之防抖
// 防抖
function debounce(fn, delay = 300) {
let timer; //闭包引用的外界变量
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 2、如何解决循环输出问题
for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}
1
2
3
4
5
2
3
4
5
这段代码从控制台输出结果是5个6
问题1、为什么都是6?
因为setTimeOut是宏任务,因为JS单线程的eventloop机制,在主线程同步任务执行完成后才会去执行宏任务,因此循环结束之后才会去执行SetTimeOut回调函数
setTimeOut函数是一个闭包,根据作用域链网上找的的话,它的父级作用域就是window,变量i就是window上的全局变量,开始执行setTimeOut之前i就已经变成6了,所以最终输出5个6
问题2、如何让它一次输出12345?
- 使用ES6中的let 因为ES6之后JS有了块级作用域,一个{}就是一个块级作用域,使用let定义变量会使i变为块级局部变量
for(let i = 1; i <= 5; i++){
setTimeout(function() {
console.log(i);
},0)
}
1
2
3
4
5
2
3
4
5
- 匿名函数(IIFE,立即执行函数) 当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
1
2
3
4
5
6
7
2
3
4
5
6
7
- 使用定时器的第三个参数(定时器回调函数的参数)
for(var i=1;i<=5;i++){
setTimeout(function(j) {
console.log(j)
}, 0, i)
}
1
2
3
4
5
2
3
4
5