文章目录
- 闭包
- 一、什么是闭包
- 二、产生闭包的条件
- 三、闭包变量存储的位置
- JS 堆栈内存释放
- 四、常见的闭包
- 闭包1:将一个函数作为另一个函数的返回值
- 闭包2. 将函数作为实参传递给另一个函数调用
- 闭包3.函数作为参数
- 闭包4. IIFE(自执行函数)
- 闭包5. 循环赋值
- 闭包6.节流防抖
- 闭包7.函数柯里化
- 五、闭包的作用
- 六、闭包的生命周期
- 闭包何时被销毁?-垃圾回收机制
- 七、闭包的缺点及解决
- 八、内存溢出和内存泄露
- 内存溢出
- 内存泄漏
- 九、经典面试题
闭包
提问:
- 什么是闭包?
- 闭包有哪些实际运用场景?
- 闭包是如何产生的?
- 闭包产生的变量如何被回收?
一、什么是闭包
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。
二、产生闭包的条件
1.函数嵌套
2.内部函数引用了外部函数的数据(变量/函数)。
PS:还有一个条件是外部函数被调用,内部函数被声明。比如:
function fn1() {
var a = 2
var b = 'abc'
function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数)
console.log(a)
}
}
fn1();
function fn3() {
var a = 3
var fun4 = function () { //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前
console.log(a)
}
}
fn3();
三、闭包变量存储的位置
直接说明:闭包中的变量存储的位置是堆内存。
- 假如闭包中的变量存储在栈内存中,那么栈的回收 会把处于栈顶的变量自动回收。所以闭包中的变量如果处于栈中那么变量被销毁后,闭包中的变量就没有了。所以闭包引用的变量是出于堆内存中的。
JS 堆栈内存释放
- 堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串。
- 堆内存释放:将引用类型的空间地址变量赋值成
null
,或没有变量占用堆内存了浏览器就会释放掉这个地址 - 栈内存:提供代码执行的环境和存储基本类型值。
- 栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。
但栈内存的释放也有特殊情况:① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。② 全局下的栈内存只有页面被关闭的时候才会被释放
四、常见的闭包
- 将一个函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数调用。
闭包1:将一个函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。
上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。
也就是说,要看闭包对象创建了一个,就看:外部函数执行了几次(与内部函数执行几次无关)。
闭包2. 将函数作为实参传递给另一个函数调用
使用回调函数就是在使用闭包
function showDelay(msg, time) {
setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
alert(msg)
}, time)
}
showDelay('atguigu', 2000)
上面的代码中,闭包是里面的function,因为它是嵌套的子函数,而且引用了外部函数的变量msg。
闭包3.函数作为参数
var a = 'cba'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 输出
* foo
/
使用 return
fo
返回回来,fo()
就是闭包,f(foo())
执行的参数就是函数fo
,因为fo() 中的 a
的上级作用域就是函数foo()
,所以输出就是foo
闭包4. IIFE(自执行函数)
var n = 'cba';
(function p(){
console.log(n)
})()
/* 输出
* cba
/
同样也是产生了闭包p()
,存在 window
下的引用 n
。
闭包5. 循环赋值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
闭包6.节流防抖
// 防抖
function debounce(fn, delay = 300) {
//默认300毫秒
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args); // 改变this指向为调用debounce所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debounce(() => {
console.log(111);
}, 1000)
);
// 节流
// 设置一个标志
function throttle(fn, delay) {
let flag = true;
return () => {
if (!flag) return;
flag = false;
timer = setTimeout(() => {
fn();
flag = true;
}, delay);
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log(111);
}, 1000)
);
闭包7.函数柯里化
function currying(fn, ...args) {
const length = fn.length;
let allArgs = [...args];
const res = (...newArgs) => {
allArgs = [...allArgs, ...newArgs];
if (allArgs.length === length) {
return fn(...allArgs);
} else {
return res;
}
};
return res;
}
// 用法如下:
// const add = (a, b, c) => a + b + c;
// const a = currying(add, 1);
// console.log(a(2,3))
五、闭包的作用
- 作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
- 作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
六、闭包的生命周期
- 产生: 嵌套内部函数fn2被声明时就产生了(不是在调用)
- 死亡: 嵌套的内部函数成为垃圾对象时。(比如f = null,就可以让f成为垃圾对象。意思是,此时f不再引用闭包这个对象了
闭包何时被销毁?-垃圾回收机制
什么时候闭包被销毁?
最终结论-如果没有特殊的垃圾回收算法(暂时没有搜索到有这种算法)会造成闭包常驻!除非手动设置为null 否则就会造成内存泄露!
七、闭包的缺点及解决
缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。
解决:能不用闭包就不用,及时释放。比如:
f = null; // 让内部函数成为垃圾对象 -->回收闭包
总而言之,你需要它,就是优点;你不需要它,就成了缺点。
八、内存溢出和内存泄露
内存溢出
内存溢出:一种程序运行出现的错误。当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误。
代码举例:
var obj = {};
for (var i = 0; i < 10000; i++) {
obj[i] = new Array(10000000); //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间
console.log("-----");
}
内存泄漏
内存泄漏:占用的内存没有及时释放。
注意,内存泄露的次数积累多了,就容易导致内存溢出。
常见的内存泄露:
- 1.意外的全局变量
- 2.没有及时清理的计时器或回调函数
- 3.闭包
情况1举例:
// 意外的全局变量
function fn() {
a = new Array(10000000);
console.log(a);
}
fn();
情况2举例:
// 没有及时清理的计时器或回调函数
var intervalId = setInterval(function () { //启动循环定时器后不清理
console.log('----')
}, 1000)
// clearInterval(intervalId); //清理定时器
情况3举例:
<script type="text/javascript">
function fn1() {
var a = 4;
function fn2() {
console.log(++a)
}
return fn2
}
var f = fn1()
f()
// f = null //让内部函数成为垃圾对象-->回收闭包
</script>
九、经典面试题
for(var i = 0;i < 5;i++) {
setTimeout(function() {
console.log(i++);
},4000)
}
console.log(i)
因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 5 了,所以会输出一堆 5。
- setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
- 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 5 了,因此最后输出的连续就都是 5。
解决办法
- 第一种是使用闭包的方式
相当于使用了立即执行函数(IIFE)
当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行 。
for(var i=0;i<5;i++){
(function(x) {
setTimeout(function() {
console.log(x++)
},4000)
})(i);
}
解释:
首先创建全局执行上下文,进入for循环,第一轮循环需要执行立即执行函数,于是创建立即执行函数的上下文,这里i=0
,那么x=0
,因为是异步,浏览器将处理的结果放入任务队列,也就是将数值0放入任务队列,此时立即执行函数的上下文从栈中弹出,回到全局执行上下文,进入第二次for循环,此时i=1
,需要执行立即执行函数,创建立即执行函数的上下文,将数值1放入任务队列,立即执行函数的上下文从栈中弹出,后面的三次循环一样。
当全局的代码执行完毕后,执行异步代码,此时将任务队列里面的数值依次输出。
- 第二种就是使用
setTimeout
的第三个参数,这个参数会被当成timer
函数的参数传入。
for (var i = 1; i < 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
4000,
i
)
}
- 第三种就是使用
let
定义i
了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <5 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 4000)
}