# 对 JavaScript 闭包的回顾
闭包,老生常谈的话题之一了,面试中我们经常被问到,在平时写代码的时候我们或多或少也是会用到他。# 什么是闭包
- 在mdn (opens new window)的解释是:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
- 实际例子
function init() {
var name = 'closure'; // name 是一个被 init 创建的局部变量
function displayName() {
// displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量,但是name又不存在于displayName函数的局部变量中
}
return displayName;
}
const initFn = init();
initFn();
有的同学可能会辩论这不是一个闭包,但是你不相信我,mdn (opens new window)可以相信吧哈哈哈,其实我想说不认同这个例子的同学,可能只是局限于对闭包的偏见,为什么呢?
- 在我的理解中,我更愿意把闭包归结为可以突破变量局部作用域的界限,拿到不属于本身作用域内的变量的函数
- 举个栗子把:子函数访问父函数的变量,由于父函数的变量存在引用,即使创建它的上下文已经销毁,它仍然存在(内部函数从父函数中返回),从而形成了闭包。闭包就是指有权访问另一个函数作用域中的变量的函数(返回函数也是闭包函数,不能释放内存)
- 让我们再来一个栗子把,依然是来权威的《JavaScript 权威指南》
const param = 'bigParam';
function getParam() {
const param = 'smallParam';
return function f() {
return param;
};
}
const documentFn = getParam();
documentFn(); //smallParam
在这段代码运行时,执行上下文栈和执行上下文的逻辑如下:
- 创建全局执行上下文,并将其入栈
- 执行 getParam 函数,创建 getParam 函数执行上下文并将其入栈
- getParam 执行上下文初始化,创建变量对象、作用域链、this 等
- getParam 函数执行完毕,getParam 执行上下文出栈
- 执行 documentFn 函数,创建 documentFn 函数执行上下文并将其入栈
- documentFn 执行上下文初始化,创建变量对象、作用域链、this 等
- documentFn 函数执行完毕,documentFn 执行上下文出栈
那么,当 getParam 上下文出栈即被销毁时,为何我们还能通过 documentFn 读取到 param 值为 smallParam 呢? 这就得涉及到我们的垃圾回收机制了。
- 在垃圾回收机制中有一个策略是
引用计数算法
,它把对象是否不再需要 简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,那么我们这里存在对 getParam 函数作用域内 param 的引用,自然这个变量就会被保留下来,从而实现了闭包这个概念。
# 闭包的优点
- 延迟变量的生命周期,如果执行上下文不被销毁,这个变量就可以一直存在
- 形成私有作用域,使得某些变量不受外部干扰
- 突破函数独立作用域,可以从外部拿到里面的变量
# 缺点
- 显而易见的是内存泄漏,因为绕过了垃圾回收机制,这个变量一直存在,过度使用闭包会导致内存占用过多
# 闭包的应用场景(这里仅个人理解)
- return 一个引用父级变量的函数
- 函数作为参数
- 自调用函数
- 循环赋值
- 回调函数
- 防抖节流
- 函数柯里化
# 简单补充 JS 堆栈
- 堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串。
- 栈内存:提供代码执行的环境和存储基本类型值。
- 堆内存释放:将引用类型的空间地址变量赋值成 null,或没有变量占用堆内存了浏览器就会释放掉这个地址
- 栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。