提问:
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
在 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();
直接说明:闭包中的变量存储的位置是堆内存。
null
,或没有变量占用堆内存了浏览器就会释放掉这个地址但栈内存的释放也有特殊情况:① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。② 全局下的栈内存只有页面被关闭的时候才会被释放
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就消失了。
上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。
也就是说,要看闭包对象创建了一个,就看:外部函数执行了几次(与内部函数执行几次无关)。
使用回调函数就是在使用闭包
function showDelay(msg, time) {
setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
alert(msg)
}, time)
}
showDelay('atguigu', 2000)
上面的代码中,闭包是里面的function,因为它是嵌套的子函数,而且引用了外部函数的变量msg。
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
var n = 'cba';
(function p(){
console.log(n)
})()
/* 输出
* cba
/
同样也是产生了闭包p()
,存在 window
下的引用 n
。
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
// 防抖
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)
);
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)
}