
JS - 执行上下文/作用域链/闭包
对于JavaScript
中执行上下文
、作用域
、闭包
等概念的一些思考。
执行上下文
执行上下文
(Execution Context)用一句话说就是JS代码执行时的抽象环境
。
例如在函数被 调用
时会生成一个函数执行上下文
,就是这个函数的环境,这里强调 调用
,就是说是运行时的动态过程。
举个栗子,一个国家就好比是一个执行上下文
,公民就好比是变量
,许多公民(不出境的那种)在国家这个范围内活动;再往下执行上下文
有嵌套,那中国就可以比作全局执行上下文
,浙江省就是第一层函数执行上下文
,杭州市就是第二层函数执行上下文
。
三种类型
JS中一共有三种执行上下文类型:
全局执行上下文: 默认的最底层环境,任何不在
函数
或eval
内部的代码都属于全局执行上下文
,一段程序中只有一个全局执行上下文。函数执行上下文: 当一个函数被
调用
时,就会创建一个函数执行上下文
,并进行一些准备工作。eval执行上下文: 执行eval函数时会创建一个
eval执行上下文
,类似于函数执行上下文
。因为实际中用到eval的情况比较少,下文不再讨论。
以下代码创建的执行上下文如图所示:
|

执行栈
在JS中,是以栈
(后进先出)这种数据结构存储执行上下文
的,也就是我们所说的 执行栈
。
执行JS代码时,JS引擎首先会创建一个全局执行上下文
,并压入栈底;之后每当 调用
一个函数时,都会为该函数创建一个函数执行上下文
,并压入栈。
之后引擎会执行栈顶的函数,函数执行结束后,该函数执行上下文
从栈中弹出;再执行下一个函数,直到全部出栈。
还是这段代码,执行栈如图所示:
|

执行过程
执行上下文的执行过程分为两个阶段:创建阶段
和执行阶段
,这部分内容后面会解释到。
创建阶段: 例如调用函数时,在执行代码前,将创建执行上下文,会处理三件事:
绑定this
→创建词法环境
→创建变量环境
执行阶段: 对变量进行赋值,执行代码。
作用域链
静态作用域
作用域是指程序代码中定义变量的区域,JS中采用的是静态作用域
(词法作用域)。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
还是上面的栗子,执行上下文
是国家这个概念上的环境,作用域
就是这个国家的实际作用范围;比如中国就是雄鸡这块版图,人民群众在没办护照的情况下是不能跑到国外去的。
静态作用域 vs 动态作用域
静态作用域: 作用域在函数定义时决定
动态作用域: 作用域在函数调用时决定
因为JS是静态作用域
,所以直接看代码中变量的定义位置即可,而不是看调用过程。
在以下代码中,运行会输出 1
|
作用域链
在代码中,我们经常会进行函数嵌套,作用域
会随着嵌套结构向顶层链接,形成作用域链
。这个概念和原型链
类似。
还是看代码:
|
在函数bb()作用域
中并没有定义变量a
和b
,所以会通过作用域链
往上层查找,在父函数aa()作用域
中找到a
,在全局作用域
中找到b
,最后输出。
闭包
上文我们介绍了执行上下文
和作用域链
,终于到了闭包
。
ES5中变量只有两种作用域:函数外部,全局变量
;函数内部,局部变量
。且变量提升机制还会将变量声明提升到它所在作用域的顶端去执行。
函数内部的变量都是局部变量,在函数外部是无法访问的。
闭包其实就是充当一个桥梁的作用,从外部可以间接地访问到内部的变量。简单来讲,就是在一个函数内部返回另外函数,通过返回的函数去获取主函数内部的局部变量。
先看一段代码:
|
在outer
函数中定义的private
局部变量,在函数外部直接访问是无法访问到的。但是我们在函数中能够访问这个局部变量,并通过一个匿名函数
返回,在外边调用outer()
接收这个匿名函数,并且赋值给外部的inner
变量,再调用inner()
即可获取到内部的private
局部变量了。
打个比方,函数就好比是一个集装箱,外面的人拿不到里面的物资。这时候甩出来一个小背包,装着里面的部分物资,外面的人就可以在小背包中拿到了。
闭包可以隐藏函数内部细节,界定公共变量 (public)
和私有变量 (private)
。配合匿名函数
和立即执行函数
,常用来实现ES5中的模块化。
原理解析
正常函数执行完毕后,里面声明的变量被垃圾回收处理掉,但是闭包可以让作用域里的变量,在函数执行完之后依旧保持没有被垃圾回收处理掉,从而被外部访问。因此闭包也可以将临时变量“缓存”在内存中。
内存泄漏
前面提到闭包会暂存变量,不被垃圾回收,很直接的就会想到,闭包可能会造成内存泄漏。(很多文章都会这么说,包括我在面试中也讲到过。)
其实这个说法是不确切的,内存泄漏是指不再使用的变量没有被释放
。而闭包函数中的局部变量
,在之后是有可能使用的,并不是“垃圾变量”,所以称不上内存泄漏。
可以这么说,这一特性导致了闭包将会消耗额外的内存,因此谨慎使用。
个人体会
文中其实我还隐藏了很多细节。比如:执行上下文的概念对象ExecutionContext
、作用域链中的[[scope]]
等。有兴趣的话应该逐一地去深入了解。(详见文末链接)
执行上下文
、作用域链
、闭包
、this
等概念。之前我是通过看面经的方式了解了点皮毛,所以面试过程中总是讲不清楚,一问一答就结束了。(我看你是完全不懂嘛)
后来我看了它们的实现原理,将它们理了一遍,感觉茅塞顿开,面试的时候就可以滔滔不绝了。其实这些概念都是一环扣一环的,串起来理解就会很直观。
参考文献
[译] 理解 JavaScript 中的执行上下文和执行栈
JavaScript深入之词法作用域和动态作用域
JavaScript深入之作用域链
我从来不理解JavaScript闭包,直到有人这样向我解释它…
- 本文作者:zhaoo
- 本文链接:https://www.izhaoo.com/2020/04/12/js-closure/index.html
- 版权声明:本博客所有文章均采用 BY-NC-SA 许可协议,转载请注明出处!
