前言
写本《JavaScript简餐》系列文章的目的是记录在阅读学习《JavaScript高级程序设计(第4版)》一书时出现的各个知识点。虽是对读书的笔记和总结,但是希望它轻量、简洁、犀利,不会引起阅读疲劳,可以在碎片化时间和闲暇之余轻巧地沐浴一下知识点。每篇文章只针对一个小部分进行讲解式的梳理,来达到个人复习总结和分享知识的目的。
一、闭包概念
闭包:指的是那些引用了另一个函数作用域中变量的函数。通常是在嵌套函数中实现的。例如:function Person() {
let age = 29;
return function sayAge() {
console.log(age);
};
}
在这里,内部函数sayAge函数引用了外部函数Perosn的变量age,即应用了另一个函数作用域中的变量。所以,sayAge函数就是一个闭包。
二、闭包的副作用
一般函数在执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。(在这里要说明一下,函数执行上下文中会有一个包含其中变量的对象,这个对象中包含函数的arguments、函数局部变量以及函数中定义的对象,我们称这个对象为变量对象。而在函数执行时,这个变量对象就称为活动对象。)但是当你定义一个闭包后,由于闭包会保留外部函数的作用域,导致外部函数的活动对象不能在它执行完毕后销毁,此时闭包的作用域链中仍然有对外部函数活动对象的引用。来看一个例子:function Person() {
let age = 29;
addAge = function () { // addAge没有用var、let或者const声明,所以相当于定义在全局的函数
++age;
};
return function sayAge() {
console.log(age);
};
}
let result = Person();
result(); // 29
addAge();
result(); // 30
在这里,我们先运行result()(要知道我们的这个result函数等于sayAge函数),得到了age的值29,之后我们借助addAge()将age的值加1,再次运行result()时我们发现age的值变成30了。这足以证明,Person函数的局部变量age一直存在于内存中,并没有在调用后被销毁。这就会导致垃圾回收程序无法回收这个变量,从而引发内存泄漏。
function Person() {
let age = 29;
addAge = function () { // addAge没有用var、let或者const声明,所以相当于定义在全局的函数
++age;
};
return function sayAge() {
console.log(age);
};
}
let result = Person();
result(); // 29
result = null; // 解除对函数的引用,这样就可以释放内存
闭包会保留包含它们的函数的作用域,所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用,因此建议仅在十分必要时使用。
三、闭包的用途
1.在函数外部读取函数的内部变量
众所周知,在函数外部时不可以调用函数内部定义的变量的。但是通过闭包我们可以访问到函数的内部变量。具体原理是在函数中将闭包作为返回值返回,之后在函数外部再次调用这个闭包即可。例如:function Person() {
let age = 29;
return function sayAge() {
console.log(age);
};
}
let result = Person(); // 29
//当然也可以这样调用:
Person()();
2.利用外部函数变量缓存值
以防抖函数为例:
function debounce(fn, wait = 1000) {
let timer;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
};
}
这里利用闭包,让外部函数定义的timer一直存在于内存中不被销毁,当防抖函数再次执行时就可以直接从内存中读取timer来判断是否还存在,从而来进行防抖限制。