闭包的定义

如果一个内层函数访问了外层函数作用域里的变量,那么即使外层函数执行完销毁了,内层函数依然能访问到当初那些外层变量,这个 内层函数 + 被它捕获的外层变量 的集合就叫做 闭包

闭包不是特意创造的功能。在 ES 标准里,没有单独定义闭包这个语法,只是定义了词法作用域和函数的传递规则,闭包是实现这些规则时的自然而然的产物:如果一个函数被拿到它定义的作用域之外执行,而它又需要访问定义时父作用域的变量,JS 引擎就必须保留这些外层变量,不能随着外层函数执行完就销毁,否则函数在外面执行的时候就找不到变量了。

只要一个语言支持满足以下两个条件的函数,就可以认为支持闭包:

  1. 函数可以作为参数、返回值传递,也可以赋值给变量。
  2. 函数可以捕获定义时所在作用域的变量,哪怕原作用域已经销毁,这些被捕获的变量依然可以被访问。
ECMAScript 与 JavaScript

ES 是 ECMAScript 的缩写,是由国际标准化组织 ECMA 国际 制定的脚本语言的通用规范标准,它不绑定任何具体的宿主环境,只规定脚本语言最核心的语法、特性、规则。

JavaScript 是 ES 规范 最知名、最主流的实现,它在 ES 核心规范的基础上,扩展了不同宿主环境的专属 API:除了完全实现 ES 的所有标准特性外,JS 还会根据运行环境的不同,额外提供环境相关的能力。

两者的关系是 先有 JS 实现,后有 ES 标准,标准是基于 JS 制定的。

闭包的产生条件

只要同时满足以下3点,闭包就会自动产生,不需要任何特殊语法:

  1. 函数嵌套:存在内层函数和外层函数,内层函数定义在外层函数的作用域内。
  2. 内层函数访问外层作用域的变量:内层函数引用了外层函数(或更上层作用域)中的变量。
  3. 内层函数被拿到外层作用域执行:内层函数被作为返回值返回、赋值给外部变量、或者作为参数传递到外层作用域,最终在它定义的作用域之外执行。

闭包的核心是 访问定义时的父作用域变量,如果函数定义在全局作用域,父作用域就是全局,本身就是全局可访问的,不存在闭包的概念。

闭包的存在形式

  1. 外层函数返回内层函数,同时保留对上层变量的引用
1
2
3
4
5
6
7
8
9
10
function outer() {
const count = 0;
return () => {
console.log(++count);
};
}

const counter = outer();
counter(); // 1
counter(); // 2
  1. 把内层函数传递到外层作用域执行,就会形成闭包
1
2
3
4
5
6
7
8
function outer() {
const name = "name";
// 把内层函数作为参数传给setTimeout,定时器会在全局作用域执行这个回调
setTimeout(function () {
console.log(name); // 依然能访问到外层的name
}, 1000);
}
outer();

setTimeout 的回调函数是在 全局执行上下文 中被执行的。

类似的场景还有事件绑定、接口请求回调等,只要回调函数定义在某个作用域内,并且访问了该作用域的变量,就会形成闭包。

  1. 高阶函数封装创建闭包
1
2
3
4
5
6
7
8
9
// 柯里化求和函数,通过闭包保留已传入的参数
function currySum(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
console.log(currySum(1)(2)(3)); // 6

防抖、节流、柯里化这类高阶函数,本质都是通过闭包保留状态。

注意事项

被闭包引用的变量不会被垃圾回收机制回收,如果闭包一直存在,这些变量就会一直占着内存。

闭包的核心是 保留外层作用域的变量,如果使用不当会导致内存泄漏。不需要用到闭包的时候及时解除引用,比如事件解绑、把闭包变量赋值为null,让垃圾回收可以正常清理。


  1. 事件循环
  2. 垃圾回收
  3. 异步编程
  4. 遍历
  5. 闭包