Skip to content
目录

作用域&上下文

在文章开始前必须要知道作用域和上下文的区别

  • 作用域

    作用域(Scope)指的是在程序中定义变量的区域,它决定了变量的可见性和生命周期。作用域规定了在代码中访问变量的规则。在 JavaScript 中,主要有全局作用域和函数作用域。全局作用域中的变量可以在整个程序中访问,而函数作用域中的变量则只能在函数内部访问。作用域链用于在嵌套的作用域中查找变量。

  • 上下文

    上下文(Context)指的是代码执行时所处的环境,它包含了当前代码的执行状态和相关信息。在 JavaScript 中,主要有全局上下文和函数上下文。全局上下文是在程序启动时创建的,而函数上下文是在函数调用时创建的。每个上下文都有自己的变量对象(Variable Object)用于存储变量、函数和其他相关信息。


作用域和上下文之间的关系是这样的:

作用域决定了变量的可见性和访问规则,而上下文提供了执行代码所需的环境和状态。作用域规定了在代码中如何查找变量,而上下文则提供了变量的具体值和其他执行时的信息。

简单来说,作用域关注的是变量的可见性和访问规则,而上下文关注的是代码执行时的环境和状态。

上下文:

在每个函数开始之初,执行上下文中会创建变量环境词法环境(还有一个私有环境,因为使用较少就不展开),并将当前函数执行上下文推入执行上下文栈中,当函数执行结束后会让执行上下文栈出栈

  • 变量环境
    • 环境记录:包含var声明和function声明的变量
    • 外部环境引用
  • 词法环境
    • 环境记录:包含letconst声明的变量
    • 外部环境引用

外部环境的引用借用概念就等同于链表的.next指针,那么.value就为环境记录了

  • 环境记录

    环境记录(Environment Record)是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构,定义标识符与特定变量和函数的关联。环境记录通常与 ECMAScript 代码的某些特定语法结构相关联。

    例如函数声明(FunctionDeclaration)、块级作用域(BlockStatement) 或 try...catch(TryStatement) 的 Catch 子句。每次评估这些代码时,都会创建一个新的环境记录(Environment Record),以记录该代码创建的标识符绑定。

    每个环境记录都有一个[[OuterEnv]]字段,该字段要么为空,要么是对外部环境记录的引用。这个字段用于模拟环境记录值的逻辑嵌套。内部环境记录的外部引用是对逻辑上围绕内部环境记录的环境记录的引用。当然,外部环境记录也可以有自己的外部环境记录。一个环境记录可以作为多个内部环境记录的外部环境。

    例如,如果一个 FunctionDeclaration 包含两个嵌套的 FunctionDeclaration,那么每个嵌套函数的环境记录都将以周围函数的当前评估环境记录作为其外部环境记录。

    这段官方文档的解释与现代JavaScript教程存在出入,具体在于——外部环境引用是存在于 环境中 还是 环境记录 中,为了统一认知理解,这里我们统一以现代JavaScript教程的在 环境中 为例讲解


    环境记录纯粹是一种规范机制,无需与 ECMAScript 实现中的任何具体工件相对应。ECMAScript 程序不可能直接访问或管理环境记录。


核心要点:

varfunction声明的变量会存入最近的变量环境constlet声明的变量会存入最近的词法环境


一个上下文中的变量会有三个状态:

  1. 声明(Declaration):当在代码中使用 varletconst 或函数声明时,会进行变量的声明。在这个阶段,变量的标识符被引入到当前作用域中,但尚未分配内存空间,也没有初始化值。
  2. 创建(Creation):在进入变量的作用域时,会创建该变量的内存空间。这个阶段被称为变量的创建阶段。在创建阶段,变量会被分配内存空间,并绑定到相应的作用域中。
  3. 赋值(Assignment):在变量创建后,可以对变量进行赋值操作。赋值阶段是给变量分配一个具体的值或引用的过程。变量的赋值可以在声明的同时进行,也可以在稍后的代码中进行。

对于 varfunction 声明的变量,会在作用域的顶部进行提升(hoisting),即在变量的作用域开始之前就进行了创建

对于 letconst 声明的变量,则是在块级作用域中实际声明的位置进行创建。

这里还有一点,function提升的优先级是高于var

javascript
console.log(typeof a);// function
var a = 2
function a() { return 1 }
1
2
3

作用域:

作用域总共分为三种:

  • 全局作用域

  • 函数作用域

  • 块级作用域

全局作用域和函数作用域都是老生常谈的问题了,除此之外的由{}包裹的就是块级作用域。

  • 当程序执行到函数时会创建函数执行上下文,然后会创建变量环境和词法环境

  • 当程序在函数中执行到块级作用域时,不会创建新的上下文,只会创建一个新的词法环境

    该词法环境的环境记录会存储块级作用域中由constvar声明的变量,outer指向当前外层(也许是函数执行上下文)的词法环境,当块级作用域部分执行结束后,词法环境会被销毁

举例如下:

ts
const name = "luowei";

function test1(){
    console.log(name); // luowei
}

function test2(){
    const name = "passionfruit"
    test1();
}

function main() {
  console.log(a); // undefined
  console.log(b); // undefined
  {
    const test1 = 2;
    var a = 1
    function b() {
      test2();
    }
    console.log(test1); // 2
  }
  b();
}

main();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

以上面这个函数为例我们逐步分析函数执行过程:

  1. 编译并创建全局执行上下文,初始化词法环境和变量环境

  2. 进行全局执行上下文的变量声明

    js
    // 全局变量环境
    const globalVariableEnvironment = {
    	test1: undefined,
    	test2: undefined,
    	main: undefined
    }
    
    // 全局词法环境
    const globalLexicalEnvironment = {
    	name: <uninitialization>
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  3. 进行全局执行上下文的变量创建

    js
    // 全局变量环境
    const globalVariableEnvironment = {
    	test1: undefined,
    	test2: undefined,
    	main: undefined
    }
    
    // 全局词法环境
    const globalLexicalEnvironment = {
    	name: undefined
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  4. 进行全局执行上下文的变量赋值

    js
    // 全局变量环境
    const globalVariableEnvironment = {
    	test1: function(){...},
    	test2: function(){...},
    	main: function(){...}
    }
    
    // 全局词法环境
    const globalLexicalEnvironment = {
    	name: "luowei"
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  5. 执行main函数

  6. 进行main函数执行上下文的变量声明

    js
    // main变量环境
    const mainFnVariableEnvironment = {
        a: undefined,
        b: undefined
    }
    
    // main词法环境
    const mainFnLexicalEnvironment = {
    	[[outer]]: ...
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  7. ab进行console.log

  8. 遇到{}块级作用域

  9. 进行块级作用域词法环境的初始化

    js
    // 块级作用域词法环境
    const blockLexicalEnvironment = {
    	test: <uninitialization>
    	[[outer]]: ...
    }
    
    1
    2
    3
    4
    5
  10. 进行main函数执行上下文的变量赋值

    js
    // main变量环境
    const mainFnVariableEnvironment = {
        // a b均被变量提升至最近的变量环境
        a: 1,
        b: function(){...}
    }
        
    // main词法环境
    const mainFnLexicalEnvironment = {
    	[[outer]]: ...
    }
    
    // 块级作用域词法环境
    const blockLexicalEnvironment = {
    	test: 2,
    	[[outer]]: mainFnLexicalEnvironment
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  11. 执行b函数 blockLexicalEnvironment被销毁

  12. 后略...

变量查找时,从自身的词法环境环境记录开始沿着outer查找,这就是作用域链

这里其实有一个问题:

既然是延词法环境链向上查找,那存在于变量环境中(也就是var声明的值)怎么被查找?

按照我的理解,在在某个环境中进行查找时,如果同时存在词法环境和变量环境,会进行一个合并查找


重点讲一下为什么test2函数调用test1时,name值为"luowei",或者说为什么test1的执行上下文中词法环境的outer指向全局执行上下文的词法环境

这里我们要说一个新名词叫词法作用域

词法作用域就是指作用域是由代码中的函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符

1.8

所以,词法作用域是编译阶段就决定好的,和函数怎么调用的没有一点关系


那闭包又是什么?

我相信前面看完这个问题也很清晰了,闭包产生后函数的执行上下文就没有出栈,如果闭包返回了了一个函数,这个函数执行上下文中的词法环境和变量环境的outer指向声明时的函数上下文中词法环境和变量环境

在JavaScript中,根据词法作用域的规则,内部函数总是可以访问外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包

所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。 ——《现代JavaScript教程》


参考文章:

ECMAScript 2024

现代JavaScript教程

浏览器工作原理与实践[李兵]——极客时间

Released under the MIT License.