JavaScript的大山:闭包和作用域链
执行上下文
所谓的程序其实可以理解为对变量的读写操作,因此便会产生一个问题:变量从哪来?执行上下文可分为两种:全局执行上下文和函数执行上下文。
从广义上来说,执行上下文由Lexical Environment和This binding构成。
Lexical Environment翻译成中文即词汇环境,而This Binding则指的是当前执行上下文中的this指向
全局执行上下文
这是一个最基础的上下文环境,其组成部分包含:全局对象(在浏览器中即Window对象)、全局Scope和outer。
在该作用域中,outer为null。因为他是为后边函数执行上下文所服务的。
浏览器中,全局执行上下文中的this
指向Window对象。
构建执行上下文
执行上下文的创建是在,代码执行之前完成创建的。例如:
处理声明
检查重复定义
scope中如果存在重复声明,则抛出错误。但全局对象中可以重复。
创建绑定
对变量进行初始赋值。
var
声明会将变量初始值赋值为undefined
函数声明会创建函数对象,然后将变量指向该对象
函数对象是一个很特殊的对象:
- 有一个特殊的
prototype
属性- [[scope]]属性:指向当前的执行上下文
- 有一个特殊属性保存函数体内容
函数执行上下文
函数执行上下文与全局执行上下文生成流程类似,但函数执行上下文中this可以通过方法进行变更。
该执行上下文中没有全局对象,只包含outer
和Scope
。
outer
可以理解为指向外部作用域,而scope
指向当前函数的作用域。
作用域链
了解上面介绍的执行上下文,其实作用域链也很简单。实际上其描述的是一种关系。例如如下代码:
1 | function b() { |
- 进行全局执行上下文的创建,将a和b函数声明存入到Window对象
- 调用a函数
- 进行函数执行上下文的创建,在该函数作用域内声明b函数和c函数
- 执行a函数的函数体,调用b函数。此时该函数作用域包含b函数,因此会打印c的函数体
什么是闭包
闭包允许函数访问并操作函数外部的变量。
只要变量或函数存在于声明函数时的作用域内,闭包即可使函数能够访问这些变量或函数。
1 | function numberFunction() { |
函数numberFunction
内部的变量number
被定义在了函数内部,因此外部无法直接修改这个变量。而这时又定义能够操作该变量的两个函数,并将其返回。这样外部只要调用这个函数并保存该返回值,即可保存该作用域链,该作用域链包含闭包的全部信息。
因此,虽然闭包是非常有用的,但不能过度使用。使用闭包时,所有的信息都会存储在内存中,直到 JavaScript 引擎确保这些信息不再使用(可以安全地进行垃圾回收)或页面卸载时,才会清理这些信息。
闭包是 JavaScript 作用域规则的副作用。当函数创建时所在的作用域消失后,仍然能够调用函数
到此,闭包的概念其实已经讲清楚了:假如一个函数能访问外部的变量,那么就形成了一个闭包,而不是一定要返回一个函数
其他
关于变量提升
首先来看一个例子,解释下什么是变量提升:
1 | console.log(a) |
从直觉上来说,第一行打印一个未声明的变量,其应该抛出错误:该变量不存在。但实际上并不会,这是因为在代码运行时,首先进行词法分析(即创建执行上下文):
执行前:
- 声明a变量,并保存在全局上下文
- 检查重复声明
- 为变量赋值初始值,将a的初始值设置为undefined
代码执行:
由于已经声明了a变量,初始值为undefined,因此打印出undefined
。
因此,导致出现变量提升现象的原因为:代码执行前首先进行此法分析,将声明的变量进行保存。
暂时性死区
所谓的暂时性死区其实是由于解析到let
const
声明符时,会将其存入scope区域,而不是存入Window对象。
并且解析时还会存入其行数,这样在该声明之前调用该变量则会导致变量不存在。