【js进阶】-执行上下文/执行上下文栈

本文深入探讨JavaScript的执行上下文,包括全局上下文、函数上下文及其创建过程。执行上下文分为解释阶段和执行阶段,其中变量对象(VO/AO)和作用域链是关键。作用域链在查找变量时起作用,从当前上下文到全局上下文逐层搜索。此外,文章还通过实例解释了变量声明、函数声明的处理顺序以及this的绑定。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、什么是执行上下文?

由于js是解释性语言,所以执行代码时会分两个阶段:一个是解释阶段,还有一个是执行阶段,解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。

简而言之,执行上下文是在执行阶段产生的,是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

注意:js在遇到一些可执行代码的时候,就会做一些准备工作,这个“准备工作” 标准来讲就是我们今天说的 “执行上下文”

二、执行上下文的类型

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文 : 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

三、什么是执行栈

它是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文;

让我们通过下面的代码示例来理解:

let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

在这里插入图片描述
上述代码的执行上下文栈。

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

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

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

四、每个执行上下文是如何创建的(底层原理)

这里我们以函数执行上下文为例

对于每个执行上下文,都有三个重要属性:

  1. 变量对象(Variable object,VO)
  2. 作用域链(Scope chain)
  3. this
变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

进入执行上下文中,这个变量对象就会被激活,此时我们叫他活动变量,活动对象是在进入函数上下文时刻被创建的,一般执行上下文中的代码分两个阶段处理(分析、执行)

  • 分析阶段

当进入执行上下文时,这时候还没有执行代码,

变量对象会包括:

  • arguments属性
  • 函数的所有形参 (如果是函数上下文),由名称和对应值组成的一个变量对象的属性被创建,没有实参,属性值设为 undefined
  • 函数声明,由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建,如果变量对象已经存在相同名称的属性,则完全替换这个属性
  • 变量声明,由名称和对应值(undefined)组成一个变量对象的属性被创建;如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

举个例子:

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

foo(1);

在进入执行上下文后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}
  • 执行阶段

    在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值,

    还是上面的例子,当代码执行完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}
总结
  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值,注意,这里是分先后顺序的
  4. 在代码执行阶段,会再次修改变量对象的属性值
巩固例子
function foo() {
    console.log(a);
    a = 1;
}

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???

代码解析:第一行的foo()执行后会报错(Uncaught ReferenceError: a is not defined。),为什么呢?这是因为函数中的 “a” 并没有通过 var 关键字声明,所有不会被存放在 AO 中。当执行那条语句的时候,此时的AO内部值为AO = { arguments: { length: 0 }},只有那个初始化的arguments属性,它表示当前函数没有形参;没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。 第二行打印结果为1,当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。

再来一题

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

代码解析:结果会打印输出函数(注意是函数而不是执行函数输出’foo’)而不是1,为什么?因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

作用域链

概念理解:

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的

1、函数创建

函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];
2、函数激活

当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。

这时候执行上下文的作用域链,我们命名为 Scope:

Scope = [AO].concat([[Scope]]);

至此,作用域链创建完毕。

五、捋一捋

以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

执行过程如下:

1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]

checkscope.[[scope]] = [
    globalContext.VO
];

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈,此时你可以理解为只创建了一个执行环境,里面啥也没有

ECStack = [
    checkscopeContext,
    globalContext
];

3.checkscope 函数上下文创建完毕后,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

checkscopeContext = {
    Scope: checkscope.[[scope]],
}

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

5.第三步:将活动对象压入 checkscope 作用域链顶端

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];

在这里插入图片描述

const grpFilters = [...new Set(fcStatus.value.map(item => item.GRP))].map(value => ({ text: value, value })) 在运行此行语句时出现了如下的报错: ERROR Cannot read properties of null (reading 'map') TypeError: Cannot read properties of null (reading 'map') at setup (webpack-internal:///./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/views/FcStatusView.vue?vue&type=script&lang=js:46:51) at callWithErrorHandling (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:387:19) at setupStatefulComponent (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:7161:25) at setupComponent (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:7122:36) at mountComponent (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:4835:7) at processComponent (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:4813:9) at patch (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:4488:11) at ReactiveEffect.componentUpdateFn [as fn] (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:5011:9) at ReactiveEffect.run (webpack-internal:///./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js:323:19) at ReactiveEffect.runIfDirty (webpack-internal:///./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js:359:12) 应该如何解决?
03-26
<think>好的,我现在需要解决用户遇到的JavaScript错误:`Cannot read properties of null (reading 'map')`。用户提供的代码是`const grpFilters = [...new Set(fcStatus.value.map(item => item.GRP))].map(value => ({ text: value, value }))`,错误发生在执行这一行时。 首先,我需要理解错误的原因。错误信息指出无法读取null的属性'map',这意味着`fcStatus.value`是null,而尝试在null上调用map方法导致了错误。因为map是数组的方法,所以当`fcStatus.value`不是数组时就会出错。 接下来,我需要考虑用户代码的上下文。用户可能在Vue的setup函数中使用这段代码,并且`fcStatus`可能是一个响应式引用(ref)或响应式对象(reactive)。根据之前的对话历史,用户之前询问过setup函数的写法,所以可能是在Vue 3的环境下开发。 接下来,我需要分析可能的情况导致`fcStatus.value`为null。例如,如果`fcStatus`是从API异步获取的数据,可能在初始化时还未获取到数据,此时其值可能为null或undefined。或者在组件传递props时,没有正确初始化fcStatus,导致其值为null。 接下来,我需要找出解决办法。关键点在于确保`fcStatus.value`是一个数组,然后再进行map操作。常见的解决方案包括: 1. **可选链操作符(Optional Chaining)**:使用`?.`来避免在null或undefined上调用方法。例如`fcStatus.value?.map(...)`,但这样可能导致后续的new Set()接收undefined,展开时会出错,所以需要处理这种情况。 2. **空值合并运算符(Nullish Coalescing)**:结合可选链和空值合并,为map提供一个默认的空数组,例如`fcStatus.value?.map(...) ?? []`,这样当fcStatus.value为null或undefined时,使用空数组避免错误。 3. **类型检查**:在代码中添加条件判断,确保fcStatus.value存在且为数组,然后再执行后续操作。 另外,还需要考虑响应式数据的问题。如果`fcStatus`是一个响应式引用,用户可能在数据加载完成之前访问了它的value,导致其为null。因此,处理异步数据的情况也很重要。 然后,我需要考虑代码的健壮性。除了处理null的情况,还应检查数组中的每个元素是否包含GRP属性,避免在item.GRP时出现undefined的错误。 可能的改进代码: 使用可选链和空值合并: ```javascript const grpFilters = [...new Set(fcStatus.value?.map(item => item.GRP) ?? [])].map(value => ({ text: value, value })); ``` 或者添加条件判断: ```javascript const grpFilters = fcStatus.value ? [...new Set(fcStatus.value.map(item => item.GRP))].map(value => ({ text: value, value })) : []; ``` 此外,如果`fcStatus`是响应式数据,可能需要使用计算属性来确保在数据更新时重新计算grpFilters,同时处理空值情况: ```javascript const grpFilters = computed(() => { if (!fcStatus.value) return []; return [...new Set(fcStatus.value.map(item => item.GRP))].map(value => ({ text: value, value })); }); ``` 还需要考虑错误处理,比如捕获可能的异常,或者添加类型校验,确保`fcStatus.value`是数组类型。如果数据来自props,应验证props的类型,确保传入的是有效数据。 总结解决方案步骤: 1. 检查`fcStatus.value`是否为null或undefined,使用可选链和空值合并处理。 2. 确保在数据不可用时提供一个默认的空数组,避免map方法调用失败。 3. 如果使用Vue的响应式系统,考虑使用计算属性,并添加必要的条件判断。 4. 添加类型校验和错误处理,增强代码的健壮性。 最后,需要向用户解释这些步骤,并提供具体的代码示例,帮助他们解决问题。</think>以下是针对该错误的分析和解决方案,按优先级分步骤说明: --- ### 错误原因定位 $$ \text{错误根源} = \begin{cases} fcStatus.value = null \\ \text{或} \\ fcStatus.value \text{未正确初始化} \end{cases} $$ --- ### 解决方案(分层次说明) #### 第一优先级:立即修复方案 **使用安全访问操作符:** ```javascript const grpFilters = [ ...new Set(fcStatus.value?.map(item => item.GRP) || []) // 关键修改点 ].map(value => ({ text: value, value })) ``` **数学表达解释:** $$ \text{安全值} = \begin{cases} fcStatus.value.map(...) & \text{if } fcStatus.value \neq null \\ [] & \text{otherwise} \end{cases} $$ --- #### 第二优先级:防御性编程增强 ```javascript // 最佳实践写法 const grpFilters = [ ...new Set( (Array.isArray(fcStatus.value) ? fcStatus.value : [] ).map(item => item?.GRP ?? '') // 双重保护 ) ].filter(Boolean) // 过滤空值 .map(value => ({ text: value, value })) ``` **执行流程图解:** ```mermaid graph TD A[fcStatus.value] --> B{是否为数组?} B -->|是| C[执行map操作] B -->|否| D[返回空数组] C --> E[提取GRP字段] E --> F{GRP是否存在?} F -->|是| G[保留值] F -->|否| H[替换为空字符串] G --> I[创建Set去重] H --> I I --> J[展开为数组] J --> K[过滤空值] K --> L[生成最终对象] ``` --- #### 第三优先级:根本问题修复 需要检查数据来源的初始化过程: 1. **如果使用Vue3 Composition API:** ```javascript // 正确初始化方式 import { ref } from 'vue' // 初始化应为空数组而非null const fcStatus = ref([]) // 异步数据获取示例 async function loadData() { try { const res = await fetch('/api/data') fcStatus.value = await res.json() || [] // 确保最终是数组 } catch { fcStatus.value = [] } } ``` 2. **如果是props传递:** ```javascript // 父组件 <ChildComponent :fc-status="initialData" /> // 子组件 props: { fcStatus: { type: Array, default: () => [] // 必须设置默认值 } } ``` --- ### 错误处理方案对比表 | 方法 | 优点 | 缺点 | 推荐场景 | |------|------|------|----------| | 可选链(?.) | 简洁 | 无法处理undefined | 简单数据校验 | | 空值合并(??) | 精准识别null/undefined | 需ES2020支持 | 现代浏览器环境 | | 类型检查(Array.isArray) | 彻底验证类型 | 代码量稍多 | 关键数据处理 | | try-catch | 捕获所有异常 | 性能损耗 | 不可靠数据源 | --- ### 进阶调试建议 1. **添加堆跟踪:** ```javascript console.trace('fcStatus当前值:', fcStatus.value) ``` 2. **类型监控函数:** ```javascript const typeWatcher = () => { console.log('实时类型检测:', { valueType: typeof fcStatus.value, isArray: Array.isArray(fcStatus.value), isNull: fcStatus.value === null }) } // 在可能修改fcStatus的地方调用 typeWatcher() ``` --- ### 最终解决方案代码 **推荐完整实现:** ```javascript const grpFilters = computed(() => { // 三级防御校验 const baseArray = Array.isArray(fcStatus.value) ? fcStatus.value : [] return [ ...new Set( baseArray .map(item => item?.GRP ?? '') // 处理缺失GRP字段 .filter(v => v !== '') // 过滤空值 ) ].map(value => ({ text: value, value, // 可添加更多元数据 count: baseArray.filter(item => item.GRP === value).length })) }) ``` **该方案具备:** - 类型安全校验(Array.isArray) - 空值处理(?. 和 ??) - 数据过滤(filter) - 响应式更新(computed) - 扩展性(可添加统计功能) 通过以上多层级防护,可彻底解决当前错误并提升代码健壮性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ronychen’s blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值