执行上下文

48 阅读34分钟

执行上下文

  1. 执行上下文的定义

    1.   什么是执行上下文?它在 JavaScript 中有哪些类型?

      面试回答(专业简洁版):

    执行上下文(Execution Context)是 JavaScript 引擎在执行一段代码时所创建的环境,用于管理变量、函数、作用域链、this 绑定等信息。你可以把它理解为“代码运行时的上下文快照”。

      JavaScript 中主要有 三种类型的执行上下文:

      1. 全局执行上下文(Global Execution Context)
    • 创建时机:JavaScript 程序启动时,最先创建;

    • 作用:

      • 创建全局对象(浏览器中是 window,Node.js 中是 global);
      • this 指向全局对象(非严格模式下);
      • 初始化全局变量和函数声明。
    • 唯一性:整个程序只有一个全局执行上下文。


      2. 函数执行上下文(Function Execution Context)
    • 创建时机:每次调用函数时动态创建;

    • 特点:

      • 每次函数调用都会创建一个新的执行上下文;
      • 包含该函数的局部变量、参数、作用域链、this 值等;
      • 函数执行完毕后,该上下文通常被销毁(除非存在闭包)。
    • 数量:可以有多个,取决于函数被调用的次数。


      3. Eval 函数执行上下文(Eval Execution Context)
    • 创建时机:当代码中使用 eval() 执行字符串时创建;

    • 特点:

      • 行为类似函数上下文,但作用域取决于 eval 调用的位置;
      • 不推荐使用,因其破坏作用域安全、影响性能、难以调试。
    • 现代开发中几乎不会遇到,但在规范中仍存在。


      补充:执行上下文的生命周期(加分项)

      每个执行上下文的创建分为两个阶段:

    1. 创建阶段(Creation Phase):

      1. 建立作用域链;
      2. 确定 this 值;
      3. 进行变量提升(Variable Hoisting)——函数声明和 var 变量被提升。
    2. 执行阶段(Execution Phase):

      1. 逐行执行代码,赋值、调用函数等。

      这些上下文会被压入 执行上下文栈(也叫调用栈,Call Stack),遵循“先进后出”原则。


      总结(一句话):

    执行上下文是 JavaScript 运行时管理代码执行环境的机制,分为全局、函数和 eval 三种类型,其中最常用的是前两种。理解它有助于掌握变量提升、作用域、闭包和 this 等核心概念。

    1.   执行上下文在代码执行时是如何被创建和销毁的?

      面试回答(专业流畅版):

    JavaScript 引擎在执行代码时,会通过 执行上下文栈(Execution Context Stack,也叫调用栈)来管理执行上下文的创建与销毁。整个过程遵循“先进后出(LIFO)”的原则,具体分为以下几个阶段:


      一、创建阶段:代码开始执行前
      1. 全局上下文首先被创建
    • 当 JS 脚本加载时,引擎首先创建 全局执行上下文;

    • 它被压入执行上下文栈的底部;

    • 此时进行:

      • 创建全局对象(如 window);
      • 绑定全局 this
      • 变量提升(Hoisting):函数声明和 var 变量被提升到作用域顶部。
      2. 遇到函数调用时,创建函数上下文
    • 每当执行到一个函数调用(如 foo()),引擎会:

      • 创建一个新的函数执行上下文;

      • 进入该上下文的创建阶段:

        1. 确定 this 值(根据调用方式);
        2. 创建词法环境(Lexical Environment),包含形参、局部变量、内部函数声明;
        3. 建立作用域链(指向外层上下文的词法环境);
      • 将该上下文压入调用栈顶部;

      • 开始执行阶段:逐行运行函数体代码。

    📌 注意:eval 上下文同理,但现代开发中极少使用。


      二、销毁阶段:函数执行完毕后
    • 当函数执行完成(遇到 return 或执行到最后),其对应的函数执行上下文会被弹出调用栈;
    • 引擎会销毁该上下文,释放其占用的内存;
    • 例外:闭包 如果该函数返回了一个内部函数(闭包),且该闭包在外部被引用,那么原函数的词法环境不会被销毁,而是被闭包持有,从而延长生命周期。

      js

      编辑

    function outer() {
      const data = 'secret';
      return function inner() {
        console.log(data); // 闭包引用了 outer 的变量
      };
    }
    const fn = outer(); // outer 执行上下文本应销毁,但因闭包引用,data 仍保留
    

      三、全局上下文的销毁
    • 全局执行上下文只有在整个程序结束时才会销毁,比如:

      • 页面关闭;
      • Node.js 进程退出。

      四、可视化流程(调用栈示例)

      js

      编辑

    function a() { b(); }
    function b() { c(); }
    function c() { console.log('done'); }
    
    a();
    

      调用栈变化:

    1. 全局上下文入栈;
    2. 调用 a()a 上下文入栈;
    3. a 调用 b()b 上下文入栈;
    4. b 调用 c()c 上下文入栈;
    5. c 执行完 → 弹出;
    6. b 执行完 → 弹出;
    7. a 执行完 → 弹出;
    8. 程序结束 → 全局上下文销毁。

      总结(一句话收尾):

    执行上下文在函数调用时创建、执行完毕后销毁(闭包除外),由调用栈统一管理。理解这一机制,是掌握作用域、闭包、内存管理等高级特性的基础。

  1. 执行上下文的生命周期

    1.   执行上下文的创建和执行过程有哪些阶段?(例如,创建阶段和执行阶段)

      面试回答(专业简洁版):

    JavaScript 的每个执行上下文(无论是全局还是函数)在创建和运行时,都会经历两个明确的阶段:创建阶段(Creation Phase)和执行阶段(Execution Phase)。这两个阶段共同决定了变量、函数、作用域和 this 的行为。


      一、创建阶段(Creation Phase)

      在代码真正执行前,JavaScript 引擎会先进行“预处理”,完成以下关键工作:

      1. 确定 this 的值
    • 全局上下文:this 指向全局对象(如浏览器中的 window,非严格模式下);
    • 函数上下文:根据调用方式绑定 this(如 obj.fn()this = obj)。
      2. 创建词法环境(Lexical Environment)
    • 词法环境包含:

      • 环境记录(Environment Record):存储变量、函数声明、形参等;
      • 对外层词法环境的引用:用于构建作用域链。
    • 此阶段会进行 变量提升(Hoisting):

      • 函数声明被完整提升(可提前调用);
      • var 声明被提升并初始化为 undefined
      • let/const 被提升但不初始化,处于“暂时性死区”(TDZ)。
      3. 建立作用域链
    • 当前上下文可通过词法环境的外层引用,逐层向上查找变量,直到全局环境。

    ✅ 此阶段不执行任何赋值或逻辑代码,只做“准备工作”。


      二、执行阶段(Execution Phase)

      在创建阶段完成后,引擎开始逐行执行代码:

      1. 变量赋值
    • 执行 let x = 10var y = 20 等赋值语句;
    • 初始化 let/const(走出 TDZ)。
      2. 函数调用
    • 遇到函数调用时,递归进入新上下文的创建 → 执行流程;
    • 新上下文被压入调用栈(Call Stack)。
      3. 表达式求值与副作用
    • 运算、DOM 操作、网络请求等在此阶段发生。

    ✅ 此阶段如果遇到未声明变量赋值(如 a = 1),在非严格模式下会意外创建全局变量,这也是内存泄漏的常见原因。


      举个例子说明两个阶段:

      js

      编辑

    console.log(a); // undefined(var 被提升)
    console.log(b); // ReferenceError(let 在 TDZ)
    var a = 1;
    let b = 2;
    function foo() {
      console.log('foo');
    }
    foo(); // 可正常调用(函数声明被提升)
    
    • 创建阶段:

      • a 被提升为 undefined
      • b 被提升但未初始化(TDZ);
      • foo 函数被完整提升。
    • 执行阶段:

      • 逐行执行 console.log、赋值、函数调用。

      总结(一句话):

    执行上下文先在创建阶段完成 this 绑定、变量提升和作用域链构建,再在执行阶段进行赋值和逻辑运算。理解这两个阶段,是掌握 hoisting、TDZ、闭包等核心概念的关键。

      v8引擎如何执行作用域链

      在 V8 引擎中,作用域链(Scope Chain)的执行并不是通过运行时“链式查找对象”实现的,而是通过编译阶段的静态分析 + 运行时的“上下文链(Context Chain)”高效访问变量。下面从原理到实现逐步解释:


      一、核心概念:作用域 vs 上下文(Scope vs Context)
    • 作用域(Scope) :是代码结构层面的概念(如函数、块级 {}),在编译阶段确定。

    • 上下文(Context) :是运行时数据结构,用于存储该作用域中定义的变量(如 varlet、函数声明等)。

    ✅ V8 在执行时,用“上下文链”来实现作用域链的语义


      二、V8 如何构建上下文链?

      示例代码:

    var globalVar = 'global';
    
    function outer() {
      let outerVar = 'outer';
      function inner() {
        let innerVar = 'inner';
        console.log(globalVar, outerVar, innerVar);
      }
      return inner;
    }
    
    const fn = outer();
    fn(); // 访问三层变量
    

      V8 执行流程:

    1. 全局上下文(Global Context)创建

      1. 存储 globalVar
      2. 是所有上下文的根
    2. 调用 outer() → 创建 outer 的函数上下文(Function Context)

      1. 存储 outerVar
      2. 其内部有一个指针指向外层上下文(即全局上下文)
    3. 调用 inner() → 创建 inner 的函数上下文

      1. 存储 innerVar
      2. 指向 outer 的上下文
    4. inner 访问 globalVar 时:

      1. V8 不会动态遍历“作用域链”,而是在编译阶段就确定了每个变量的“作用域层级”和“偏移量”

      2. 运行时直接通过上下文链指针跳转,快速定位变量。

    🔧 内部结构简化表示:

    innerContext = {
      variables: { innerVar: 'inner' },
      outer: outerContext   // ← 指向外层上下文
    }
    
    outerContext = {
      variables: { outerVar: 'outer' },
      outer: globalContext
    }
    
    globalContext = {
      variables: { globalVar: 'global' },
      outer: null
    }
    

      三、关键优化:作用域信息在编译期静态确定

      V8 的 Ignition 字节码生成器 在解析函数时,会为每个变量标记其作用域深度(scope depth)槽位(slot index)

      例如,在 inner 中访问 outerVar

    • 编译器知道:outerVar 在“上一层上下文”的第 0 个槽位;

    • 生成的字节码可能是:LdaContextSlot [depth=1, index=0]

      • depth=1:跳 1 层上下文(当前 → outer)
      • index=0:取该上下文的第 0 个变量

      ✅ 无需运行时遍历链表,直接按“坐标”取值!


      四、不同变量类型的处理差异
    变量类型存储位置查找方式
    var / 函数声明存入对应函数的 Context 对象通过上下文链访问
    let / const(块级)存入 块级 Context(如果有闭包或 eval)同上
    简单局部变量(无闭包引用)直接分配到栈或寄存器(优化后不进 Context)字节码直接操作,最快

    💡 V8 会尽可能避免创建 Context。如果一个函数内部的变量没有被内部函数闭包引用,V8 会将其优化为栈上局部变量,不放入 Context,从而提升性能。


      五、闭包如何影响上下文链?

      当内部函数引用外部变量时,V8 必须保留外层上下文,防止被垃圾回收。

    function outer() {
      let x = 1;
      return function inner() {
        return x; // 引用了 x → outer 的上下文不能释放
      };
    }
    
    • inner 的上下文持有对外层 outer 上下文的引用;
    • 即使 outer() 已返回,其上下文仍存活(形成闭包);
    • 这就是“上下文链”在内存中的体现。

      六、总结:V8 执行作用域链的关键机制
    阶段操作
    编译期分析作用域结构,为每个变量分配作用域深度和槽位
    运行时创建 Context 对象存储变量,通过指针链接成上下文链
    变量访问根据编译期确定的 depth 和 index,直接跳转取值
    优化未被闭包引用的局部变量不进 Context,直接栈分配

    🎯 一句话总结V8 通过“编译期静态分析 + 运行时上下文链 + 字节码直接寻址”高效实现作用域链,而非低效的运行时对象遍历。

      这种设计使得 JavaScript 的词法作用域在高性能引擎中也能快速执行。

    1.   请描述执行上下文在创建阶段进行的三个步骤:变量对象的创建、作用域链的建立、this 的绑定。

      面试回答(专业清晰版):

    在执行上下文的创建阶段(Creation Phase),JavaScript 引擎会完成三个关键步骤:变量对象的创建、作用域链的建立、this 的绑定。这三个步骤共同为后续代码执行准备好运行环境。


      1. 变量对象的创建(Variable Object, VO)
    • 目的:收集当前上下文中所有变量、函数声明和形参,形成一个“变量容器”。

    • 具体行为:

      • 函数上下文:

        • 形参(arguments)被加入;
        • 函数内部的 function 声明被提升并初始化(可提前调用);
        • var 声明被提升,初始值为 undefined
        • let/const 被记录但不初始化(处于“暂时性死区”)。
      • 全局上下文:

        • 全局变量和函数声明被收集;

        • 变量对象就是全局对象(如浏览器中的 window)。

    • 📌 注:在 ES5+ 规范中,“变量对象”已被 词法环境的环境记录(Environment Record) 所取代,但概念本质一致。

      2. 作用域链的建立(Scope Chain)
    • 目的:确定当前上下文如何查找变量。

    • 构建方式:

      • 作用域链是一个对象列表(或词法环境链);
      • 首先包含当前上下文的变量对象;
      • 然后依次向上链接到外层函数的变量对象,直到全局上下文。
    • 示例:

    • js

    • 编辑

    function outer() {
      var a = 1;
      function inner() {
        console.log(a); // 查找顺序:inner VO → outer VO → global VO
      }
    }
    
    • 作用域链在函数定义时就已确定(词法作用域),而非调用时。

      3. this 的绑定(This Value Determination)
    • 目的:确定当前上下文中 this 关键字指向哪个对象。

    • 绑定规则:

      • 全局上下文:

        • 非严格模式:this 指向全局对象(window / global);
        • 严格模式:thisundefined
      • 函数上下文:

        • 由调用方式决定:

          • obj.fn()this = obj
          • fn()(独立调用)→ this = 全局对象(非严格)或 undefined(严格);
          • 箭头函数:不绑定自己的 this,继承外层上下文的 this
        • 可通过 call/apply/bind 显式指定。


      补充说明(加分项):

    虽然“变量对象”是 ES3/ES5 早期术语,但其核心思想在现代规范中依然存在——只是被更精确的 词法环境(Lexical Environment) 和 变量环境(Variable Environment) 所替代。 例如:

    • 词法环境:用于 let/const/function 声明,支持块级作用域;
    • 变量环境:用于 var 声明,保持函数作用域。

      总结(一句话):

    执行上下文在创建阶段通过 变量对象收集声明、作用域链确定查找路径、this 绑定确定上下文对象,为后续代码执行奠定基础。理解这三步,是掌握 hoisting、闭包、this 指向等核心机制的关键。

  1. 变量对象(Variable Object)

    1.   什么是变量对象?它在执行上下文中的作用是什么?

      面试回答(清晰准确版):

    变量对象(Variable Object, VO)是 JavaScript 在执行上下文创建阶段生成的一个内部抽象对象,用于存储当前作用域中所有变量、函数声明和形参的引用。它的核心作用是:为代码执行提供变量查找的“容器”。


      一、变量对象在不同上下文中的表现
    执行上下文类型变量对象的具体形式
    全局上下文全局对象本身(如浏览器中的 window)
    函数上下文活动对象(Activation Object, AO),是变量对象在函数执行时的特例

    ✅ 例如,在函数中:

    js

    编辑

    function foo(a) {
      var b = 2;
      function c() {}
    }
    

    其变量对象(即活动对象 AO)会包含:abcarguments 等属性。


      二、变量对象的核心作用
    1. 收集声明(Hoisting 的基础)

      1. 在创建阶段,所有 var 声明和 function 声明都会被“提升”并注册到变量对象中;
      2. var 初始化为 undefinedfunction 被完整赋值;
      3. 这就是“变量提升”现象的底层原因。
    2. 作为作用域链的一环

      1. 作用域链本质上是由多个变量对象(或词法环境)组成的链表;
      2. 引擎通过作用域链从当前 VO 向外层 VO 逐级查找变量。
    3. 提供变量访问的“命名空间”

      1. 所有在当前作用域声明的标识符,都作为变量对象的属性存在;
      2. 例如:var x = 1 等价于 VO.x = 1(内部机制,不可直接访问)。

      三、变量对象 vs 现代规范(加分项)

    自 ES6 起,ECMAScript 规范用 词法环境(Lexical Environment) 替代了“变量对象”概念,原因包括:

    • 支持 let/const 的块级作用域;
    • 更精确地区分变量声明的类型和作用域行为。

    但“变量对象”的思想仍然保留:

    • 变量环境(Variable Environment):对应 var 声明;
    • 词法环境(Lexical Environment):对应 let/const/function 声明。

      总结(一句话):

    变量对象是执行上下文在创建阶段用于存储变量和函数声明的内部容器,它是理解变量提升、作用域链和执行上下文机制的关键抽象。虽然现代规范已用词法环境替代,但其核心思想依然贯穿 JavaScript 的运行模型。

    1.   变量提升是如何与变量对象关联的?在函数声明和变量声明时有何不同?

      面试回答(专业流畅版):

    变量提升(Hoisting)是 JavaScript 在执行上下文创建阶段,将变量和函数声明“移动到作用域顶部”的行为。它的底层实现依赖于变量对象(Variable Object, VO)——即在代码执行前,引擎会先构建变量对象,把声明提前注册进去。而函数声明和变量声明在提升行为上有本质区别,主要体现在初始化时机和可访问性上。


      一、变量提升与变量对象的关联
    • 在执行上下文的创建阶段,JavaScript 引擎会扫描当前作用域的代码,将所有声明(非赋值)收集到变量对象中;
    • 这个过程就是“提升”——不是物理移动代码,而是逻辑上先注册声明;
    • 因此,变量对象是变量提升的数据载体:所有被提升的标识符都作为变量对象的属性存在。

      二、函数声明 vs 变量声明的提升差异
    特性函数声明(function foo() {})变量声明(var x)
    是否提升✅ 是✅ 是
    初始化时机创建阶段就完成初始化(函数体被赋值)创建阶段仅声明,初始化为 undefined
    可访问性创建阶段结束后即可调用(“函数提升”)创建阶段结束后值为 undefined,赋值在执行阶段
    示例foo() 可在声明前调用console.log(x) 在 var x = 1 前输出 undefined
      代码示例:

      js

      编辑

      . 创建阶段(Creation Phase)
    • 扫描代码,找到所有 函数声明 和 变量声明;
    • 函数声明被完整提升:sayHello 被创建,并直接绑定到完整的函数对象(包含函数体);
    • 变量声明(如 var)只提升名字,值为 undefinedlet/const 提升但处于“暂时性死区”。
    console.log(a);      // undefined(var 提升但未赋值)
    console.log(typeof b); // "function"(函数已初始化)
    a(); // TypeError: a is not a function
    b(); // 正常执行
    var a = function() {};       // 函数表达式:只有变量 a 被提升
    function b() { console.log('ok'); }
    

    🔍 注意:函数表达式(如 var a = function() {})只有变量名 a 被提升(值为 undefined),函数体不会被提升。


      三、ES6 中的 let/const 行为(加分项)
    • let/const 也会被提升到变量对象(或更准确地说,词法环境),但不会初始化;
    • 在初始化前访问会抛出 ReferenceError,这段区域称为“暂时性死区”(TDZ);
    • 这说明:提升 ≠ 可访问,提升只是“声明注册”,是否可访问取决于初始化时机。
    console.log(x); // ReferenceError(TDZ)
    let x = 1;
    

      总结(一句话收尾):

    变量提升的本质是在创建阶段将声明注册到变量对象中,而函数声明会完整初始化,var 变量仅声明未赋值,这导致它们在代码执行前的可访问性完全不同。理解这一点,是避免“undefined”陷阱和掌握 JS 执行模型的关键。

  1. 执行上下文是如何通过作用域链来访问变量的?

JavaScript 的执行上下文(Execution Context)是代码执行时的环境,它包含变量对象(Variable Object)、作用域链(Scope Chain)和 this 值三个核心组成部分。

当 JavaScript 引擎执行一段代码时,会创建对应的执行上下文,并通过作用域链来查找变量。作用域链的本质是一个由当前执行上下文及其所有外层(父级)执行上下文的变量对象组成的链表结构。

具体过程如下:

  1. 变量查找从当前作用域开始:当代码中引用一个变量时,JavaScript 引擎首先在当前执行上下文的变量对象中查找该变量。
  2. 逐级向上查找:如果当前作用域中没有找到,引擎会沿着作用域链向上一级(即外层函数或全局作用域)的变量对象继续查找。
  3. 直到全局作用域:这个过程会一直持续,直到找到该变量,或者到达全局执行上下文仍未找到。如果最终仍未找到,就会抛出 ReferenceError。

举个例子:

javascript

编辑

var a = 1;
function outer() {
  var b = 2;
  function inner() {
    var c = 3;
    console.log(a, b, c); // 1, 2, 3
  }
  inner();
}
outer();
  • inner 函数中访问 a 时,首先在 inner 的变量对象中查找(没有);
  • 然后在 outer 的变量对象中查找(也没有);
  • 最后在全局变量对象中找到 a = 1

这个查找路径(inner → outer → global)就构成了作用域链。

关键点总结:

  • 作用域链是在函数定义时静态确定的,而不是在运行时动态生成的(这是“词法作用域”的体现);
  • 每个执行上下文都有自己的作用域链;
  • 闭包的本质就是函数保留对其定义时作用域链的引用,即使外层函数已经执行完毕。
  1. 请解释在嵌套函数的情况下,作用域链是如何建立的?

回答:

在 JavaScript 中,作用域链是在函数定义时静态确定的,而不是在运行时动态创建的。当存在嵌套函数时,每个内部函数都会捕获其外层函数的作用域,从而形成一条从内到外的作用域链。

  1. 作用域链的建立过程
  • 每个函数在被创建(定义)时,会保存对其**外部词法环境(Lexical Environment)**的引用。

  • 当该函数被调用时,JavaScript 引擎会为其创建一个执行上下文,其中包含一个作用域链。

  • 这个作用域链由以下部分组成:

    • 当前函数的变量对象(本地变量、参数等);
    • 外层函数的变量对象;
    • 一直向上,直到全局变量对象。
  1. 示例说明

javascript

编辑

var globalVar = 'global';
function outer() {
  var outerVar = 'outer';
  
  function inner() {
    var innerVar = 'inner';
    console.log(globalVar, outerVar, innerVar); // 'global', 'outer', 'inner'
  }
  
  return inner;
}

const fn = outer();
fn(); // 调用 inner
  • inner 函数在定义时,就“记住”了它所处的词法环境:即 outer 的作用域。
  • 即使 outer() 已经执行完毕并从调用栈中弹出,inner 仍然可以通过闭包访问 outerVar
  • inner 被调用时,其作用域链为:
  • text
  • 编辑
inner 的变量对象 → outer 的变量对象 → 全局变量对象

6. 关键机制:词法作用域 + 闭包

  • 词法作用域:函数的作用域由其在源代码中定义的位置决定。
  • 闭包:内部函数即使在其外层函数执行结束后,仍能访问外层函数的变量,正是因为作用域链被保留了下来。
  1. 总结
  • 嵌套函数的作用域链是在函数定义时建立的,由内向外逐层链接所有外层作用域。
  • 这条链在函数执行时用于变量查找,确保内部函数可以访问其外层作用域中的变量。
  • 这也是 JavaScript 实现闭包的基础。
  1. this 绑定

    1.   在不同的执行上下文中,this 的值是如何确定的?(例如,全局上下文、函数上下文、箭头函数)

      this 的值不是在函数定义时确定的,而是在函数被调用时根据调用方式动态绑定的。不同执行上下文中,this 的指向规则如下:


    1. 全局执行上下文

    • 在非严格模式下:this 指向全局对象(浏览器中是 window,Node.js 中是 global)。
    • 在严格模式('use strict')下:thisundefined

      javascript

      编辑

    console.log(this); // 浏览器中 → window(非严格模式)
    

    1. 普通函数调用(独立函数调用)

    • 非严格模式:this 指向全局对象。
    • 严格模式:thisundefined

      javascript

      编辑

    function foo() {
      console.log(this);
    }
    foo(); // 非严格 → window;严格 → undefined
    

    ⚠️ 注意:即使函数是在某个对象内部定义的,只要调用时没有通过对象引用,就属于普通函数调用。


    1. 作为对象方法调用

    • this 指向调用该方法的对象(即点号 . 前面的对象)。

      javascript

      编辑

    const obj = {
      name: 'Alice',
      greet() {
        console.log(this.name);
      }
    };
    obj.greet(); // 'Alice' → this 指向 obj
    

    如果将方法赋值给变量后再调用,则失去对象上下文,退化为普通函数调用:

    javascript

    编辑

    const fn = obj.greet;
    fn(); // this → window(非严格)或 undefined(严格)
    

    1. 构造函数调用(使用 new

    • this 指向新创建的实例对象。

      javascript

      编辑

    function Person(name) {
      this.name = name;
    }
    const p = new Person('Bob');
    console.log(p.name); // 'Bob'
    

    1. 箭头函数

    • 箭头函数没有自己的 this,它会继承外层作用域的 this 值(即定义时所在上下文的 this)。
    • 这个行为是静态绑定的,不受调用方式影响。

      javascript

      编辑

    const obj = {
      name: 'Charlie',
      regular() {
        const arrow = () => console.log(this.name);
        arrow(); // 'Charlie' — 箭头函数继承 regular 的 this(即 obj)
      }
    };
    obj.regular();
    

    ✅ 箭头函数常用于回调中,避免 this 丢失(如 setTimeout、数组方法等)。


    1. 显式绑定:call / apply / bind

    • 可以强制指定 this 的值。
    • callapply 立即执行函数,bind 返回一个新函数,其 this 被永久绑定。

      javascript

      编辑

    function greet() {
      console.log(`Hello, ${this.name}`);
    }
    const user = { name: 'David' };
    greet.call(user); // 'Hello, David'
    

    ⚠️ 注意:箭头函数无法通过 call/apply/bind 改变 this,因为其 this 是词法继承的。


      总结口诀(便于记忆)

    • 全局上下文:非严格 → window,严格 → undefined
    • 普通函数:看调用方式,独立调用 → 全局/undefined
    • 对象方法:谁调用 → this 指向谁
    • new 构造:this → 新实例
    • 箭头函数:没有 this,继承外层
    • call/apply/bind:显式指定(对箭头函数无效)
    1.   如何解释在不同函数调用方式下 this 的指向差异?

      回答:

      JavaScript 中的 this 并不是在函数定义时确定的,而是在函数被调用时根据调用方式动态绑定的。不同调用方式会导致 this 指向不同对象。主要分为以下几种情况:


    1. 普通函数调用(独立调用)

    • 调用形式:fn()

    • this 指向:

      • 非严格模式:全局对象(浏览器中是 window
      • 严格模式:undefined

      javascript

      编辑

    function foo() {
      console.log(this);
    }
    foo(); // 非严格 → window;严格 → undefined
    

    ✅ 关键:没有调用对象前缀,就是普通调用。


    1. 作为对象方法调用

    • 调用形式:obj.method()
    • this 指向:点号前面的对象(即调用上下文)

      javascript

      编辑

    const obj = {
      name: 'Alice',
      greet() {
        console.log(this.name);
      }
    };
    obj.greet(); // 'Alice' → this 指向 obj
    

    ⚠️ 注意:如果将方法赋值给变量再调用,会丢失上下文:

    javascript

    编辑

    const fn = obj.greet;
    fn(); // this → window(非严格)或 undefined(严格)
    

    1. 构造函数调用(使用 new

    • 调用形式:new Constructor()
    • this 指向:新创建的实例对象

      javascript

      编辑

    function Person(name) {
      this.name = name;
    }
    const p = new Person('Bob');
    console.log(p.name); // 'Bob'
    

    new 会创建一个空对象,并将其绑定为函数内部的 this


    1. 箭头函数调用

    • 调用形式:任意(但箭头函数无自己的 this
    • this 指向:继承外层词法作用域的 this(定义时确定,不可改变)

      javascript

      编辑

    const obj = {
      name: 'Charlie',
      regular() {
        const arrow = () => console.log(this.name);
        arrow(); // 'Charlie' — 箭头函数继承 regular 的 this(即 obj)
      }
    };
    obj.regular();
    

    ⚠️ 箭头函数不受调用方式影响,也无法通过 call/apply/bind 改变其 this


    1. 显式绑定调用:call / apply / bind

    • 调用形式:fn.call(obj, ...)fn.apply(obj, [...])fn.bind(obj)(...)
    • this 指向:传入的第一个参数对象

      javascript

      编辑

    function greet() {
      console.log(`Hello, ${this.name}`);
    }
    const user = { name: 'David' };
    greet.call(user); // 'Hello, David'
    

    ✅ 这是强制指定 this 的方式,优先级高于普通调用和对象方法调用(但低于 new)。


      this 绑定优先级(从高到低)

    1. new 绑定(构造函数)

    2. 显式绑定(call/apply/bind

    3. 隐式绑定(对象方法调用)

    4. 默认绑定(普通函数调用)

    📌 箭头函数不参与上述规则,它直接继承外层 this


      总结

    • this 的指向完全取决于函数如何被调用,而非定义位置(箭头函数除外)。
    • 理解不同调用方式的上下文,是避免 this 丢失或误用的关键。
    • 在实际开发中,常通过箭头函数或**bind** 来固定 this,尤其在事件回调、定时器、Promise 等异步场景中。
  1. 执行上下文栈

    1.   什么是执行上下文栈?它如何管理代码执行的顺序?

      回答:

      执行上下文栈(Execution Context Stack,简称 ECS),也叫调用栈(Call Stack),是 JavaScript 引擎用来管理代码执行顺序的一种后进先出(LIFO)的数据结构。每当 JavaScript 代码运行时,引擎会通过 ECS 来跟踪当前正在执行的函数及其上下文。


    1. 什么是执行上下文?

      执行上下文是 JavaScript 代码执行时的环境,包含:

    • 变量对象(Variable Object):存储变量、函数声明、参数等;
    • 作用域链(Scope Chain):用于变量查找;
    • this 值:当前上下文中的 this 指向。

      JavaScript 有三种执行上下文:

    • 全局执行上下文(Global Execution Context):程序启动时创建,唯一;
    • 函数执行上下文(Function Execution Context):每次调用函数时创建;
    • Eval 执行上下文(较少使用)。

    1. 执行上下文栈如何工作?

      步骤如下:

    1. 程序开始执行时,JavaScript 引擎首先创建全局执行上下文,并将其压入执行上下文栈。

    2. 当调用一个函数时,引擎为该函数创建一个新的函数执行上下文,并将其压入栈顶。

    3. 函数执行完毕后,其对应的执行上下文从栈中弹出,控制权交还给栈中下一个上下文(通常是它的调用者)。

    4. 当所有代码执行完毕,栈中只剩下全局上下文,最终也被弹出,程序结束。

    ✅ 栈顶始终是当前正在执行的上下文。


    1. 示例说明

      javascript

      编辑

    function greet() {
      sayHello();
    }
    
    function sayHello() {
      console.log('Hello!');
    }
    
    greet();
    

      执行过程如下:

    1. 全局上下文入栈;
    2. 调用 greet()greet 上下文入栈;
    3. greet 中调用 sayHello()sayHello 上下文入栈;
    4. sayHello 执行完毕 → 弹出;
    5. greet 执行完毕 → 弹出;
    6. 全局代码执行完毕 → 全局上下文弹出。

      调用栈变化:

      text

      编辑

    [Global][Global, greet][Global, greet, sayHello][Global, greet][Global][]
    

    1. 与异步、事件循环的关系

    • JavaScript 是单线程的,同步代码完全由执行上下文栈管理;

    • 异步回调(如 setTimeout、Promise)不会立即入栈,而是先放入任务队列(Task Queue);

    • 只有当执行栈为空时,事件循环(Event Loop)才会将任务队列中的回调推入栈中执行。

    🔍 这解释了为什么 setTimeout(fn, 0) 不会立即执行 —— 必须等当前栈清空。


    1. 常见问题:栈溢出(Stack Overflow)

      如果函数递归调用没有终止条件,会不断压入新上下文,最终超出栈容量,导致 “Maximum call stack size exceeded” 错误。

      javascript

      编辑

    function infinite() {
      infinite(); // 无限递归 → 栈溢出
    }
    

      总结

    • 执行上下文栈是 JavaScript 同步代码执行顺序的核心机制;

    • 它以 LIFO 方式管理上下文,确保函数按调用顺序正确执行和返回;

    • 理解 ECS 有助于掌握变量提升、作用域、闭包、异步执行等高级概念。

    1.   JavaScript 引擎如何利用执行上下文栈来实现函数的嵌套调用和返回?

      回答:

      JavaScript 引擎通过执行上下文栈(Execution Context Stack,ECS) 来管理函数的嵌套调用与返回,其核心机制基于后进先出(LIFO)的栈结构,确保每个函数在正确的作用域环境中执行,并能准确返回到调用者。


    1. 嵌套调用时:压栈(Push)

      当一个函数调用另一个函数(包括递归或深层嵌套)时,JavaScript 引擎会:

    1. 暂停当前执行上下文(但保留在栈中);

    2. 为被调用函数创建新的执行上下文,包含其变量对象、作用域链和 this

    3. 将新上下文压入执行上下文栈顶部,成为当前活跃上下文。

    ✅ 每次函数调用 = 一次压栈操作。

      示例:

      javascript

      编辑

    function A() {
      console.log('A start');
      B();
      console.log('A end');
    }
    
    function B() {
      console.log('B start');
      C();
      console.log('B end');
    }
    
    function C() {
      console.log('C');
    }
    
    A();
    

      执行上下文栈变化:

      text

      编辑

    [Global][Global, A]          // 调用 A()
    → [Global, A, B]       // A 中调用 B()
    → [Global, A, B, C]    // B 中调用 C()
    

    1. 函数执行完毕:弹栈(Pop)与返回

      当一个函数执行结束(遇到 return 或执行到底):

    1. 引擎销毁当前栈顶的执行上下文;

    2. 将其从栈中弹出;

    3. 恢复前一个上下文(即调用者)为当前执行环境;

    4. 继续执行调用点之后的代码。

    ✅ 每次函数返回 = 一次弹栈操作。

      接上例,C 执行完后:

      text

      编辑

    [Global, A, B]       // C 返回[Global, A]          // B 返回[Global]             // A 返回
    

      输出顺序:

      text

      编辑

    A start
    B start
    C
    B end
    A end
    

    1. 作用域链与上下文隔离

    • 每个函数的执行上下文都独立保存其词法环境;
    • 嵌套调用时,内层函数可通过作用域链访问外层变量(闭包原理);
    • 但各上下文的变量对象彼此隔离,避免污染。

    1. 与调用栈(Call Stack)的关系

    • 执行上下文栈在运行时的表现形式就是调用栈(Call Stack);
    • 浏览器开发者工具中的“Call Stack”面板正是 ECS 的可视化;
    • 若递归过深(如无限调用),栈空间耗尽 → “Maximum call stack size exceeded”(栈溢出)。

    1. 关键总结

    • 压栈:函数被调用时创建上下文并入栈;
    • 弹栈:函数执行完毕后上下文出栈,控制权交还调用者;
    • LIFO 顺序:保证了“谁调用我,我返回给谁”的精确控制流;
    • 嵌套深度 = 栈高度:决定了当前执行的函数层级。

      为什么这很重要?

      理解 ECS 如何管理嵌套调用,是掌握以下概念的基础:

    • 闭包(Closure)
    • 递归与栈溢出
    • 异步回调的执行时机(需等栈清空)
    • 调试时的调用栈分析
  1. 块级作用域的影响

    1.   ES6 中引入的 letconst 是如何影响执行上下文的?

      回答:

      ES6 引入的 letconst 不仅改变了变量的可变性规则,更重要的是改变了变量在执行上下文中的创建、初始化和访问方式,主要体现在以下三个方面:


    1. 引入了“块级作用域”(Block Scope)

    • 在 ES5 中,var 只有函数作用域和全局作用域;
    • let/const 引入了块级作用域(由 {} 包裹的代码块,如 iffortry/catch 等);
    • 这意味着执行上下文中的词法环境(Lexical Environment) 结构变得更细粒度。

      javascript

      编辑

    {
      let a = 1;
      const b = 2;
    }
    console.log(a); // ReferenceError: a is not defined
    

    ✅ 每个块都会创建一个新的词法环境记录(Lexical Environment Record),作为执行上下文的一部分。


    1. 变量提升行为的变化:存在“暂时性死区”(Temporal Dead Zone, TDZ)

    • var 声明会被提升(hoisted) 到函数或全局作用域顶部,并初始化为 undefined
    • let/const 虽然也会被创建(creation phase),但不会被初始化,直到代码执行到声明语句;
    • 在声明之前访问变量会抛出 ReferenceError,这段区域称为 TDZ。

      javascript

      编辑

    console.log(x); // undefined(var 提升并初始化)
    console.log(y); // ReferenceError(TDZ)
    let y = 10;
    

    🔍 执行上下文的创建阶段:

    • 对于 var:创建变量 → 初始化为 undefined
    • 对于 let/const:创建变量 → 不初始化(处于 TDZ);
    • 执行阶段遇到声明语句时,才进行初始化(const 还需立即赋值)。

    1. 执行上下文内部结构的变化:从“变量对象(VO)”到“词法环境(Lexical Environment)”

    • ES5 中,执行上下文使用 变量对象(Variable Object, VO) 存储 var 和函数声明;

    • ES6 起,规范改用 词法环境(Lexical Environment) 模型,包含:

      • 环境记录(Environment Record):

        • 声明式环境记录(Declarative Environment Record):存储 let/const/class/函数参数等;
        • 对象式环境记录(Object Environment Record):用于全局上下文,将变量映射到全局对象(仅 var 和函数声明会挂载到全局对象);
      • 对外部词法环境的引用(用于构建作用域链)。

    let/const 不会挂载到全局对象(如 window),而 var 会:

    javascript

    编辑

    var a = 1;
    let b = 2;
    console.log(window.a); // 1
    console.log(window.b); // undefined
    

    1. 对闭包和内存的影响

    • 由于块级作用域更细,闭包捕获的变量范围更精确,有助于减少内存占用;
    • 例如,在循环中使用 let,每次迭代都会创建一个新的绑定,避免经典闭包问题:

      javascript

      编辑

    for (var i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 0); // 输出 3, 3, 3
    }
    
    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 0); // 输出 0, 1, 2
    }
    

    原因:let 在每次循环体(块)中创建新的词法绑定,每个回调闭包捕获的是不同的 i


      总结:let/const 对执行上下文的核心影响
    特性varlet / const
    作用域函数/全局块级
    提升提升并初始化为 undefined提升但不初始化(TDZ)
    全局对象属性
    重复声明允许不允许(SyntaxError)
    执行上下文存储位置变量对象(VO)词法环境的声明式记录
    1.   如何解释块级作用域在执行上下文中的特殊处理?

      回答:

      ES6 引入的块级作用域(由 {} 定义,如 iffortry/catch 等)改变了 JavaScript 执行上下文的内部组织方式。与传统的函数作用域不同,块级作用域在执行上下文的词法环境(Lexical Environment) 中被单独创建和管理,具有特殊的生命周期和绑定规则。


    1. 执行上下文不再只依赖“变量对象”,而是使用“词法环境”

    • 在 ES5 中,执行上下文使用变量对象(VO) 存储 var 和函数声明,作用域只有全局和函数两级;

    • ES6 起,规范采用 词法环境(Lexical Environment) 模型,它由两部分组成:

      • 环境记录(Environment Record):存储标识符(变量、函数等);

      • 对外部词法环境的引用:用于构建作用域链。

    ✅ 每个代码块(block)都会创建一个新的词法环境,作为当前执行上下文的子环境。


    1. 块级作用域的创建时机:在进入块时动态生成

      当控制流进入一个块(如 if (true) { ... })时:

    • JavaScript 引擎会临时创建一个新的词法环境;
    • 该环境记录中包含该块内所有 letconstclass 和函数声明(在严格模式下);
    • 此环境挂载在当前执行上下文的作用域链中,位于当前函数/全局环境之下。

      javascript

      编辑

    function foo() {
      var a = 1;
      if (true) {
        let b = 2;      // 创建新的块级词法环境
        const c = 3;
        console.log(a); // 可访问外层变量(通过作用域链)
      }
      // b 和 c 在此处不可访问
    }
    

    🔍 作用域链结构示例(在块内):

    text

    编辑

    Block LexicalEnv → Function LexicalEnv → Global LexicalEnv
    

    1. “暂时性死区”(TDZ)是块级作用域的关键特征

    • 在块开始到 let/const 声明语句之间,变量处于 TDZ(Temporal Dead Zone);
    • 尽管变量在词法环境中已被创建(creation phase),但未初始化;
    • 访问 TDZ 中的变量会抛出 ReferenceError

      javascript

      编辑

    {
      console.log(x); // ReferenceError: Cannot access 'x' before initialization
      let x = 10;
    }
    

    ✅ 这说明:块级变量的“创建”和“初始化”是分离的,这是对执行上下文初始化阶段的重要扩展。


    1. 块级作用域不影响 var 和函数声明(在非严格模式下)

    • var 声明无视块级作用域,仍提升到函数或全局作用域;
    • 函数声明在块中的行为较复杂(ES6 规范中,在严格模式下也受块作用域限制,但浏览器有兼容性差异);
    • let/const/class 严格绑定到所在块。

      javascript

      编辑

    {
      var a = 1;
      let b = 2;
    }
    console.log(a); // 1(var 提升到函数/全局)
    console.log(b); // ReferenceError(b 仅存在于块内)
    

    1. 对执行上下文栈的影响:不新增执行上下文,但新增词法环境

    • 重要区别:进入一个块 不会创建新的执行上下文(只有函数调用才会);

    • 但会在当前执行上下文内部创建一个新的词法环境层级;

    • 因此,块级作用域是执行上下文内部的嵌套作用域结构,而非独立上下文。

    📌 举例:for 循环中的 let 每次迭代都会创建一个新的块级词法环境,使得闭包能捕获不同的变量值。

      javascript

      编辑

    for (let i = 0; i < 3; i++) {
      setTimeout(() => console.log(i), 0);
    }
    // 输出:0, 1, 2
    // 原因:每次循环体是一个新块,i 是独立绑定
    

      总结:块级作用域在执行上下文中的特殊处理
    方面说明
    作用域粒度从函数级细化到块级({})
    环境结构在当前执行上下文中嵌套新的词法环境
    变量生命周期let/const 在块开始时创建,声明时初始化(TDZ)
    执行上下文数量不增加 ECS 栈深度(仅函数调用才压栈)
    作用域链新块环境链接到外层环境,形成更细的作用域链
    特性letconst
    是否可重新赋值✅ 可以❌ 不可以(会报错)
    是否提升(hoisting)是(但有暂时性死区 TDZ)是(同样有 TDZ)
    作用域块级作用域 {}块级作用域 {}
    重复声明同一作用域内禁止重复声明同一作用域内禁止重复声明
    必须初始化❌ 不强制(可先声明后赋值)✅ 声明时必须赋值
  1. 常见问题与调试

    1. 如何通过理解执行上下文来排查变量未定义、this 指向错误等常见问题?
    2. 请解释某个你遇到的实际问题,以及如何通过分析执行上下文来解决它的。