此名字暂不可见
No distraction,it's you and me

作用域链与执行上下文

2025-01-21

1. 作用域和作用域链

1.1 什么是作用域

​ 根据MDN的定义,作用域是当前的执行上下文(下面在讲),值和表达式在其中“可见”或可被访问。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。

​ 简单来说,作用域就是限制一个变量在程序中的使用范围。也就是说作用域最大的用处就是隔离变量,不让变量泄露出去,不同作用域下同名变量不会有冲突。


1.2 js中的作用域模型

​ 在js中,采用的是词法作用域(在书里看到这个词感觉很高深啊),也称为静态作用域。与之相对应的还有一个动态作用域。

词法作用域:函数的作用域在函数被定义是就决定了

动态作用域:函数的作用域是在函数调用时才决定的

很好理解:

1
2
3
4
5
6
7
8
9
10
11
const value = 1
function f1(){
console.log(value)
}
function f2(){
const value = 2
f1()
}

f2()

​ 根据上面的定义,如果是词法作用域,打印的结果就是1;如果是动态作用域,打印的结果就是2.


1.3 作用域的分类

到目前为止,js中作用域分为三类:

  • 全局作用域: 在函数外部声明的变量拥有全局作用域,这意味着它们可以在代码的任何地方被访问。
  • 函数作用域: 在函数内部声明的变量只能在该函数内部被访问,这些变量对于函数外部是不可见的。
  • 块级作用域: 由let和const声明的变量在它们所在的代码块(例如if语句或for循环)内有效,而在代码块外部则无法访问。

​ 前两种作用域很好理解,用的也很多,这里笔者就不再阐释。不过关于块级作用域在书里则有一些有意思的点:除了上面的let和const,js还有其他与块级作用域相关的功能。

1.3.1 with

​ 虽然with现在已经几乎不被支持,不过它确实是块级作用域的一种形式,用with从对象中创建出的作用域仅在with声明中而非外部作用域中有效

1.3.2 try/catch

​ try/catch的分句会创建一个块级作用域,其中声明的变量仅在catch中有效。

1
2
3
4
5
try{throw 2;}catch(a){
console.log(a) //2
}

console.log(a) //undefined

1.4 作用域链

​ 简单来说,由多个执行上下文的变量对象构成的链表就叫作用域链。作用域链主要是进行标识符(在js中所有我们可以自主命名的都可以称之为 标识符,比如变量名和函数名)的查询,标识符解析就是沿着作用域链一级一级的搜索标识符的过程,而作用域链就是保证对变量和函数的有序访问。

在查找一个变量时,通常会进行以下步骤:

  • 从当前上下文的变量对象中查找

  • 如果没有找到,就会从父级执行上下文的变量对象中查找

  • 如果还是没有找到,就一直找直到全局上下文的变量对象,也就是全局对象,这就到了作用域链的顶端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const a = 1;

    function foo() {
    const b = a;
    console.log(b); // 输出 1
    }

    console.log(b); // 报错:b is not defined


    ​ 在这个例子中,变量b在函数foo内部声明,它的作用域链包括函数foo和全局作用域。由于b在函数foo内部找不到定义,所以会向全局作用域查找,找到变量a,因此b的值为1。

1.5作用域嵌套

​ 就像函数可以嵌套一样,作用域也可以多个嵌套,而作用域嵌套遵循以下的规则:

  • 内部作用域能够访问外部作用域。
  • 外部作用域无法访问内部作用域。
  • 兄弟作用域不可互相访问。

2 执行上下文

2.1 什么是执行上下文

​ 什么是执行上下文?当一个函数执行时,会创建一个被称为执行上下文的内部对象,这就像一份记录,记录了函数在哪里被调用,函数的调用方式,传入的参数等信息,一个执行上下文定义了一个函数执行时的环境。简单来说,只要有js代码运行,那么它就一定是运行在执行上下文中。

在js中,执行上下文有三种类型:

  • 全局执行上下文:只存在一个,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。
  • 函数执行上下文:可以存在无数个,当函数被调用时才会被创建,且每次函数被调用时都会创建一个新的执行上下文,它会按定义的顺序(后面再说)执行一系列步骤。
  • Eval函数执行上下文:指在eval函数中运行的代码,不过不建议使用这个这个函数,故不作讨论。

执行上下文的生命周期包括三个阶段:创建阶段→执行阶段→回收阶段,笔者这里重点介绍创建阶段。

(1)创建阶段

JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  1. 创建词法环境组件(后面再说)
  2. 创建变量环境组件 (后面再说)
  3. this绑定

(2)执行阶段

执行变量赋值、代码执行

(3)回收阶段

执行上下文出栈等待被回收

2.2 执行栈

​ 执行栈,又称为调用栈,用于存储代码执行期间创建的所有执行上下文,遵循后进先出的原则。

​ 当 Js 引擎第一次遇到我们写的代码时,它会创建一个全局执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的函数执行上下文并压入栈的顶部。

​ 引擎会优先执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

2.3 如何管理执行栈

​ 为了解释执行上下文的行为,可以将其以代码的形式阐述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let Stack = [] //假设这是执行栈,执行栈会先压入全局执行上下文
Stack.push('全局作用域')

console.log('全局作用域')//在全局执行上下文中,js会先执行全局代码,直到遇到一个函数调用
function f1(){
f2()
}

function f2(){
console.log('函数作用域')
}


f1()//这里遇到函数调用f1(),它是在全局执行上下文中被调用,因此在全局执行上下文的上方为f1加上一个函数执行上下文
Stack.push(f1)
//在f1中又调用了f2,因此在f1的函数执行上下文的上方为f2创建一个函数执行上下文
Stack.push(f2)

//遵循后进先出,先执行顶端的f2
Stack.pop()//f2执行完毕,将其移除
Stack.pop()//f1执行完毕,将其移除

​ 当上述代码在浏览器加载时,js 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 f1()函数调用时,js 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。

​ 当从 f1() 函数内部调用 f2() 函数时,js 引擎为 f2() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 f2() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 f1() 函数的执行上下文。

​ 当 f1() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。


2.4 初始阶段

2.4.1 词法环境

​ 根据GPT所言,词法环境 是 JavaScript 执行上下文中的一个重要组成部分,用来管理 letconst 声明的变量和函数声明。它是基于代码中的词法作用域来定义的,也就是说,变量的可访问性是由它们在代码中的书写位置决定的。

​ 在词法环境的内部有两个组件:

  • 环境记录器:是存储变量和函数声明的实际位置。

  • 外部环境的引用:意味着它可以访问其父级词法环境。

词法环境有两种类型:

  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。
  • 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

2.4.2 变量环境

​ 变量环境同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。

​ 在 ES6 中,词法环境组件和变量环境组件的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 var 变量绑定。

2.4.3 this绑定

​ 在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。 在函数执行上下文中,大多数情况下,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)。


程序执行全过程

​ 程序启动,全局执行上下文被创建,压入执行栈

  1. 创建全局上下文的 词法环境
    1. 创建 对象环境记录器 ,它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理 letconst 定义的变量)
    2. 创建 外部环境引用,值为 null
  2. 创建全局上下文的 变量环境
    1. 创建 对象环境记录器,它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
    2. 创建 外部环境引用,值为 null
  3. 确定 this 值为全局对象(以浏览器为例,就是 window

函数被调用,函数执行上下文被创建,压入执行栈

  1. 创建函数上下文的 词法环境
    1. 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理 letconst 定义的变量)
    2. 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
  2. 创建函数上下文的 变量环境
    1. 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
    2. 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
  3. 确定 this

进入函数执行上下文的执行阶段:

​ 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

进入函数执行上下文的回收阶段:

​ 将上下文移出执行栈,等待回收。

重复上述步骤直到全局执行上下文移出执行栈。

附录

​ js解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。

​ 作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

​ 一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个闭包(感觉下次可以讲一下,如果没人讲的话)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值

最后,简要概况一下作用域词法环境执行上下文这三者的概念:

  • 作用域:作用域就是一个独立的区域,它可以让变量不会向外暴露出去。作用域最大的用处就是隔离变量。内层作用域可以访问外层作用域。一个作用域下可能包含若干个执行上下文。
  • 词法环境:指相应代码块内标识符与变量值、函数值之间的关联关系的一种体现。词法环境内部包含环境记录器和对外部环境的引用。环境记录器是存储变量和函数声明的实际位置,对外部环境的引用意味着可以访问父级词法环境。
  • 执行上下文:JavaScript代码运行的环境。分为全局执行上下文,函数执行上下文和eval函数执行上下文(前两个较常见)。创建执行上下文时会进行this绑定、创建词法环境和变量环境。

​ 在《JavaScript权威指南》中,有两段有些意思的代码,拿来分享一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	var scope = "global scope"
function checkscope(){
var scope = "local scope"
function f(){
return scope
}
return f()
}
checkscope()
(这是分割线)-------------------------------------------------------------------------------
var scope = "global scope"
function checkscope(){
var scope = "local scope"
function f(){
return scope
}
return f
}
checkscope()()


​ 两段代码返回的结果都是“local scope”,看上去好像区别不大,但内部代码在执行时一样吗?

​ 事实上,还是有一点区别的。

1
2
3
4
5
6
7
8
9
10
11
12
	let Stack = [] //还是用这个模拟一下执行栈
//模拟第一段代码
Stack.push(checkscope)
Stack.push(f)
Stack.pop()
Stack.pop()
(这是分割线)-------------------------------------------------------------------------------
//模拟第二段代码
Stack.push(checkscope)
Stack.pop()
Stack.push(f)
Stack,pop()

3 闭包

新建文件夹

< PreviousPost
闭包,垃圾回收和内存泄露
NextPost >
react Hooks
CATALOG
  1. 1. 1. 作用域和作用域链
    1. 1.1. 1.1 什么是作用域
    2. 1.2. 1.2 js中的作用域模型
    3. 1.3. 1.3 作用域的分类
      1. 1.3.1. 1.3.1 with
      2. 1.3.2. 1.3.2 try/catch
    4. 1.4. 1.4 作用域链
    5. 1.5. 1.5作用域嵌套
  2. 2. 2 执行上下文
    1. 2.1. 2.1 什么是执行上下文
    2. 2.2. 2.2 执行栈
    3. 2.3. 2.3 如何管理执行栈
    4. 2.4. 2.4 初始阶段
      1. 2.4.1. 2.4.1 词法环境
      2. 2.4.2. 2.4.2 变量环境
      3. 2.4.3. 2.4.3 this绑定
    5. 2.5. 程序执行全过程
    6. 2.6. 附录
  3. 3. 3 闭包