执行上下文

所谓的程序其实可以理解为对变量的读写操作,因此便会产生一个问题:变量从哪来?执行上下文可分为两种:全局执行上下文和函数执行上下文。

从广义上来说,执行上下文由Lexical Environment和This binding构成。

Lexical Environment翻译成中文即词汇环境,而This Binding则指的是当前执行上下文中的this指向

全局执行上下文

img

这是一个最基础的上下文环境,其组成部分包含:全局对象(在浏览器中即Window对象)、全局Scope和outer。

在该作用域中,outer为null。因为他是为后边函数执行上下文所服务的。

浏览器中,全局执行上下文中的this指向Window对象。

image-20230304112700824

构建执行上下文

执行上下文的创建是在,代码执行之前完成创建的。例如:

image-20230304114816276

  1. 处理声明

  2. 检查重复定义

    scope中如果存在重复声明,则抛出错误。但全局对象中可以重复。

  3. 创建绑定

    对变量进行初始赋值。

    var声明会将变量初始值赋值为undefined

    函数声明会创建函数对象,然后将变量指向该对象

函数对象是一个很特殊的对象:

  • 有一个特殊的prototype属性
  • [[scope]]属性:指向当前的执行上下文
  • 有一个特殊属性保存函数体内容

函数执行上下文

函数执行上下文与全局执行上下文生成流程类似,但函数执行上下文中this可以通过方法进行变更。

该执行上下文中没有全局对象,只包含outerScope

outer可以理解为指向外部作用域,而scope指向当前函数的作用域。

作用域链

了解上面介绍的执行上下文,其实作用域链也很简单。实际上其描述的是一种关系。例如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function b() {
console.log('hello')
}

function a() {
b()

function b() {
console.log(c)
}
function c() {
console.log(b)
}
}
a()

  1. 进行全局执行上下文的创建,将a和b函数声明存入到Window对象
  2. 调用a函数
  3. 进行函数执行上下文的创建,在该函数作用域内声明b函数和c函数
  4. 执行a函数的函数体,调用b函数。此时该函数作用域包含b函数,因此会打印c的函数体

什么是闭包

闭包允许函数访问并操作函数外部的变量。

只要变量或函数存在于声明函数时的作用域内,闭包即可使函数能够访问这些变量或函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function numberFunction() {
let number = 0
function getNumber() {
return number
}
function add(num) {
number += num
}
return { getNumber, add }
}

const n = numberFunction()
console.log(n.getNumber()) // 0
n.add(1)
console.log(n.getNumber()) // 1

函数numberFunction内部的变量number被定义在了函数内部,因此外部无法直接修改这个变量。而这时又定义能够操作该变量的两个函数,并将其返回。这样外部只要调用这个函数并保存该返回值,即可保存该作用域链,该作用域链包含闭包的全部信息。

因此,虽然闭包是非常有用的,但不能过度使用。使用闭包时,所有的信息都会存储在内存中,直到 JavaScript 引擎确保这些信息不再使用(可以安全地进行垃圾回收)或页面卸载时,才会清理这些信息。

闭包是 JavaScript 作用域规则的副作用。当函数创建时所在的作用域消失后,仍然能够调用函数

到此,闭包的概念其实已经讲清楚了:假如一个函数能访问外部的变量,那么就形成了一个闭包,而不是一定要返回一个函数

其他

关于变量提升

首先来看一个例子,解释下什么是变量提升:

1
2
console.log(a)
var a = 1

从直觉上来说,第一行打印一个未声明的变量,其应该抛出错误:该变量不存在。但实际上并不会,这是因为在代码运行时,首先进行词法分析(即创建执行上下文):

执行前:

  • 声明a变量,并保存在全局上下文
  • 检查重复声明
  • 为变量赋值初始值,将a的初始值设置为undefined

代码执行:

由于已经声明了a变量,初始值为undefined,因此打印出undefined

因此,导致出现变量提升现象的原因为:代码执行前首先进行此法分析,将声明的变量进行保存。

暂时性死区

所谓的暂时性死区其实是由于解析到let const声明符时,会将其存入scope区域,而不是存入Window对象。

并且解析时还会存入其行数,这样在该声明之前调用该变量则会导致变量不存在。