前言
闭包就是函数可以访问他被创建时所处的上下文环境。本文首先简单介绍了闭包产生的原理,之后简单介绍了执行上下文(执行环境)和作用域的基本概念,以便理解闭包。
闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。(引自MDN)
简单说,闭包就是函数可以访问他被创建时所处的上下文环境。创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包本质是上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放,导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。
JS 代码运行时,父作用域中有很多与子函数无关的变量,为了不产生性能问题,作用域链中的父作用域可能会先于子作用域销毁。但是销毁父作用域不能影响子函数,所以要再创建个对象,把子函数内引用的父作用域的变量打包带走,放到一个属性上。这就是闭包的机制。
闭包并不是新语法,闭包的产生是基于执行上下文写代码时自然产生的结果,我们不需要为了闭包而写闭包。
下面会简单讲讲执行上下文和作用域链。
执行上下文(执行环境)
执行上下文是一种十分抽象的概念,称之为执行环境可能更好理解。可以理解为执行上下文声明和定义了执行 JS 代码的环境,JS 代码都是在执行上下文中运行。
执行上下文分为全局执行上下文、函数执行上下文和 eval 函数执行上下文。其中执行在 eval 函数中的代码会有属于他自己的执行下文,在此不做介绍。
JS 解析代码时会先创建一个全局执行上下文环境,会创建一个全局的 window 对象, 并且设置 this 指向全局对象。一个程序中只有一个全局执行上下文。
在函数调用之前,会创建一个函数执行上下文环境,跟全局执行上下文类似。一个程序中可以有任意多个函数执行上下文。
JS 会使用执行上下文栈来管理执行上下文。执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入栈中。每当遇到一个函数调用,就会为该函数创建一个函数执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,该函数执行上下文从栈中弹出,继续执行下一个执行上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
创建执行上下文的过程是:
- 绑定
this - 创建词法环境组件,记录变量(函数)名称与变量(函数)引用的映射。
- 创建变量环境组件,变量环境也是一个词法环境。
在 ES6 中,词法环境和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。创建执行上下文时,let 和 const 定义的变量并没有关联任何值(未初始化),但 var 定义的变量被设成了 undefined。这就是为什么可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。这就是变量声明提升。
最后,JS 完成对所有这些变量的分配,执行代码。
作用域
作用域(Scope)是当前的执行上下文,值和表达式在其中“可见”或可被访问。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。(引自MDN)
如果在自己作用域找不到所需变量就会去父级作用域查找,依次向上级作用域查找,直到访问到 window 对象就被终止,这一层层的关系就是作用域链。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
作用域有以下几种:
- 全局作用域:
window对象、最外层变量、未定义直接赋值的变量,过多的全局作用域变量会污染全局命名空间,引起命名冲突 - 函数作用域:声明在函数内部的变量
- 块级作用域:ES6中
let和const可以声明块级作用域,使用一个代码块({})的创建出来的作用域
参考资料
闭包 - JavaScript | MDN
Scope(作用域) - MDN Web 文档术语表:Web 相关术语的定义 | MDN
我从来不理解JavaScript闭包,直到有人这样向我解释它 - 掘金
JavaScript 的静态作用域链与“动态”闭包链 - 掘金
精华提炼「你不知道的 JavaScript」之作用域和闭包 - 掘金
[译] 理解 JavaScript 中的执行上下文和执行栈 - 掘金
前端面试题之JavaScript篇