执行上下文、作用域到底是什么?二者有什么关系

ops/2024/10/22 16:36:17/

在说明执行上下文和作用域之前,我们需要了解一下LHS和RHS查询,这样才能更好的理解为什么JavaScript存在执行上下文和作用域。

LHS 和 RHS

我们都知道JavaScript是一门解释型语言/动态/脚本语言,他会在执行之前一刻进行编译,然后交给JS引擎执行。

编译

编译器负责把代码解析成机器指令,通常会有三个步骤:

  1. 分词/词法解析:将JavaScript字符串分解为词法单元(token),如var a = 2=> vara=2
  2. 解析/语法分析:将一个个token的流(数组)转为抽象语法树(AST)
  3. 代码生成:将AST转为机器指令,等待执行。
执行

JS引擎拿到一堆机器指令开始执行,这个时候需要查询变量,LHSRHS就登场了。

  1. LHS (Left-hand Side):查询目的是变量赋值,如a=1,是为了将值1赋给变量a
  2. RHS (Right-hand Side):查询的目的就是查询实际值,如foo(),查找foo是函数,才能执行;如果不是函数就会抛出TypeError异常;找不到则会抛出ReferenceError异常。

而两种查询方法获取变量的都规则,就叫做 **作用域(Scope),执行上下文(Execution Context)**则包含作用域。下面我们分别介绍他们。

执行上下文

什么是执行上下文

执行上下文,其包含定义变量的 环境记录Environment Record)上下文(this),同时也控制着代码对变量的访问规则(这就是作用域),简单点说就是“代码执行的环境”。
所有的 JavaScript 代码在运行时都是在执行上下文中进行的,每创建一个执行上下文,就会将当前执行上下文放到一个栈顶,这就就是我们常说的执行栈

JavaScript 中有三种情形会创建新的执行上下文:

  • 全局执行上下文,进入全局代码的时候,也就是执行全局代码之前。
  • 函数执行上下文,函数被调用之前。
  • Eval 执行上下文,eval 函数调用之前。

执行上下文的组成

  1. 环境记录 (Environment Record)

它包含函数声明、参数和变量。之前也被成为为词法环境(Lexical Environment)

  1. 作用域

由词法环境决定,也称为静态作用域,变量在哪里定义就在哪里确定。每当 JavaScript 引擎尝试访问变量或函数时,它首先会查找当前执行上下文的变量对象。如果它在那里找不到标识,它就会沿着作用域链向上移动,检查每个父上下文的变量对象,直到找到标识符或到达全局执行上下文。如果在任何上下文中都找不到标识符,则会引发 ReferenceError。

  1. **this**

当前的代码在哪个对象下被调用,如果没有则默认是window(严格模式、箭头函数除外…)

执行上下文的生命周期

运行 JavaScript 代码时,当代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作。

创建阶段

执行上下文的创建大体步骤如下:

  1. 创建环境记录 (Environment Record),包含变量环境(VO:variable object

    • 确定函数的形参(并赋值
    • 函数环境会初始化创建 _arguments__ _对象(并赋值
    • 确定字面量形式的函数声明(并赋值
    • var定义的变量、函数表达式声明(未赋值,变量提升
    • 记录letconst定义的变量(不会声明!
  2. 确定作用域(链)

词法环境决定,哪里声明定义,就在哪里确定

  1. 确定 this 指向

this 由调用者确定箭头函数是词法决定

伪代码:

javascript">executionContextObj = {variableObject : {}, // 变量对象,里面包含 Arguments 对象,形式参数,函数和局部变量scopeChain : {},// 作用域链,包含内部上下文所有变量对象的列表this : {}// 上下文中 this 的指向对象
}
执行阶段
  1. 变量对象赋值
    • 变量赋值
    • 函数表达式赋值
  2. 调用函数
  3. 顺序执行其它代码

举个例子:

javascript">const foo = function(num){var a = "Hello";var b = function varB(){};function c(){}let d = "World"
}
foo(10);
  • 创建阶段
javascript">executionContextObj = {variableObject : {num: 1, //确定形参并且赋值arguments: {0:10, length:1}, //确定argumens c: function c(){}, //确定字面变量定义的函数a: undefined,// var 定义的局部变量,初始值为undefinedb: undefined,// var 定义的局部变量,初始值为undefined// let 定义的变量只会记录,到执行赋值的阶段这里为暂死区}, scopeChain : {},//词法解释阶段已经确定了作用域,该阶段相当于引用this : {}
}
  • 执行阶段
javascript">executionContextObj = {variableObject : {num: 1, //确定形参并且赋值arguments: {0:10, length:1}, //确定argumens c: function c(){}, //确定字面变量定义的函数a: "hello",// var 定义的局部变量,赋值b: function varB(){},// var 定义的局部变量,赋值d: 'world' //let 定义的局部变量,声明并赋值}, scopeChain : {},//词法解释阶段已经确定了作用域,该阶段相当于引用this : window //假定是全局定义
}

作用域

在MDN中,我们可以找到定义:

The scope is the current context of execution in which value and expressions are “visible” or can be referenced.

翻译一下:作用域(Scope)指的是在执行上下文中可见(或者说是可用)变量的范围
image.png
也就是它跟执行上下文看起来很像,但又不同。通常分为:

  • 全局作用域

指不在任何大括号或者函数中定义的变量

  • 函数作用域

指在函数括号内定义的变量,只能在函数内部访问,不能在函数外部访问

  • 块级作用域

块级作用域比较特殊,指在任意大括号内使用letcosnt 定义的变量,其定义的变量只能在块级内部作用域中访问。同时它是没有this的,可以认为它只保存了词法环境,保存这标识及其引用关系。

作用域链

当访问一个变量时,解释器会首先在当前作用域查找标示符,如果找到了,则使用当前作用域下的变量,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。

那这个父作用域又是那个呢?实际上是要到创建这个函数的那个域。 作用域中取值,这里强调的是“创建”,而不是“调用”,切记切记——这种类型的作用域又称为静态作用域,也被称为词法作用域,因为在词法分析时就确定了查找关系(这也是和执行上下文最大的不同!)。

javascript">function foo(){console.log(a)
}function bar(){var a = 3;foo();
}
var a = 1;
bar();

上面代码会打印 1,为什么呢?因为此处foo的函数是在全局作用域window上定义的,所以查找时现在foo函数中查找a,找不到会去window上查找,所以此处a=1

顺便在看下this的,感受下其中的不同,虽然这个与作用域无关…

javascript">function foo(){console.log(this.a)
}function bar(){var a = 2foo();
}
var a = 1;bar() // 1
bar.call({a:3}); //1

此处的两个输出会打印 1,第一个大家可能容易理解,为什么第二个也是1呢?此处foo被调用时,其执行上下文指向的依然是全局执行上下文,所以这里的this也指向window,所以此处a=1

变量提升

上面说过,JavaScript中,存在函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部,这就是我们常说的变量提升,注意:

  • 只有声明的变量会提升,值不会。
  • 严格模式下不存在变量提升。
  • letconst也存在变量提升,但是letconst定义的变量会造成暂时性死区,定义变量后一开始就形成了封闭作用域,凡是在声明之前就使用这些变量,就会报错。

varlet的声明提升:

javascript">console.log(a) //undefined
var a = 1var b = 1
{//报错,如果没有提升,不是应该显示成1?,所以是有提升console.log(b)let b = 2
}

当前作用域下只要存在变量,就算是变量提升得到,也相当于找到了,不会再去父作用域中找:

javascript">var a = 1
function foo(){console.log(a)var a = 2
}
foo()//undefined

函数定义也存在变量提升,而且是整体提升,如果是函数变量则看定义的关键字是var还是其他,这和上文保持一致:

javascript">console.log(age);
var age = 20
console.log(age);// 1.提升到最前面
function age() {}
// 2.将function改成匿名函数的方式,则不会提升
//var age = function(){}console.log(age);
// 1. 函数提升输出
//f age(){}
// 20
// 20//2. 将function改成匿名函数的方式,则不会提升(注意要重新在一个新的环境下运行)
//undefined
//20
//ƒ age(){}

参考

了解词法环境吗?它和闭包有什么联系?
[

](https://www.jianshu.com/p/4b83b97cb39e)


http://www.ppmy.cn/ops/21523.html

相关文章

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之七 简单进行人脸检测并添加面具特效实现

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之七 简单进行人脸检测并添加面具特效实现 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之七 简单进行人脸检测并添加面具特效实现 一、简单介绍 二、简单进行…

天星金融普及个人养老金制度,共筑老龄友好型社会

在人口老龄化的浪潮中,我国正面临着日益严峻的养老挑战。据国家卫健委数据显示,预计到2035年,我国60岁及以上人口占总人口的比例将超过30%,构建老龄友好型社会已成为国家发展的重要任务。在这一背景下,个人养老金制度的…

掌握Midjourney视觉艺术的关键提示词指南

在数字艺术的海洋中,Midjourney以其独特的图像生成能力脱颖而出,为艺术家和创意工作者提供了前所未有的创造自由。要真正掌握这一工具,理解并有效使用各种提示词至关重要。本文将深入探索Midjourney中的“风格关键词”、“场景关键词”、“视…

Spring Boot框架强大的事件驱动模型(ApplicationEvent)

文章目录 前言应用场景异步处理事务边界外的操作跨微服务通信系统监控与日志聚合UI更新生命周期管理工作流或业务流程缓存同步 小试牛刀定义事件实现事件处理器注册事件处理器发布事件测试事件 写在最后 前言 在Spring Boot应用中,事件处理器是指那些处理特定类型事…

SQLAlchemy的使用

SQLAlchemy中filter函数的使用 https://blog.csdn.net/m0_67093160/article/details/133318889 创建临时字段 select id , CONCAT(‘内容’) AS fullname from example_table; Pandas数据类型转换_pandas转换数据类型 https://blog.csdn.net/qq_41404557/article/details/125…

vue快速入门(四十二)ref与$refs的使用

注释很详细&#xff0c;直接上代码 上一篇 新增内容 使用ref标记元素或组件通过$refs获取元素或组件实例 源码 App.vue <template><div id"app"><!-- ref相当于为子组件或者元素绑定一个ref属性&#xff0c;然后通过this.$refs属性获取到对应的元素或…

Linux内核广泛采用的侵入式数据结构设计

Linux内核广泛采用的侵入式数据结构设计恐怕很难应用到一般程序开发中。基本上是个高维十字链表&#xff0c;一个节点(struct)可以同时位于多个hash/list/tree中。我分享下我的经历&#xff0c;我刚入行时遇到一个好公司和师父&#xff0c;给了我机会&#xff0c;一年时间从3k薪…

Kafka导航【Kafka】

Kafka导航【Kafka】 前言版权推荐Kafka随堂笔记 第三章 生产者3.4生产者分区3.4.1.分区好处3.4.2 生产者发送消息的分区策略3.4.3 自定义分区器 3.5 生产经验——生产者如何提高吞吐量3.6 生产经验——数据可靠性3.7 生产经验——数据去重3.7.1 数据传递语义3.7.2 幂等性3.7.3生…