Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wqhui opened this issue Apr 18, 2022 · 1 comment
Open

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

wqhui opened this issue Apr 18, 2022 · 1 comment

Comments

@wqhui
Copy link
Owner

wqhui commented Apr 18, 2022

LHS 和 RHS

在说明执行上下文和作用域之前,我们需要了解一下LHS和RHS查询,这样才能更好的理解为什么JavaScript存在执行上下文和作用域。
我们都知道JavaScript是一门解释型语言/动态/脚本语言,他会在执行之前一刻进行编译,然后交给JS引擎执行。

编译

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

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

执行

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

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

而两种查询方法获取变量的地方,就叫做执行上下文(也叫作用域)

什么是执行上下文

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

执行上下文的创建

何时创建执行上下文

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

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

创建执行上下文具体分析

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

  1. 创建执行上下文并推到执行栈的栈顶
  2. 绑定上下文(this)

在全局执行上下文中,this 的值指向全局对象(在浏览器中,this引用 Window 对象)。
如果是在函数执行上下文中,this的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下),除此之外,我们还可以使用callapplybind指定this

  1. 创建词法环境(Lexical Environment)和变量环境(VariableEnvironment)

语法环境是基于 ECMAScript 代码的词法嵌套结构,来定义标识符与特定变量和函数的关联关系,由环境记录(Environment Record)和可能为空引用(null)的外部词法环境组成,用于letconst的声明。变量环境继承于词法环境,区别在于其用于var声明,因为其定义的变量可以提升。(ECMA成员说明:About "LexicalEnvironment" and "VariableEnvironment"

这一步会创建变量及其关系,在全局执行上下文中(这里对两种环境不做区分),会:

  1. 会找到所有非函数中的var声明顶级函数声明顶级let const class声明块级作用域声明的变量和函数
  2. 对标识符或者说是名字的重复进行处理。
  3. 登记环境记录,var声明并初始化为undefined(同时会绑定到this),登记顶级函数并初始化并赋值,登记let const class声明但未初始化(这里也就是我们常说的变量提升)。块级作用域内部的变量和函数比较特殊,对于变量中 var变量和函数会提升(如果顶级存在同名的let cosnt class 声明则不会提升),而且二者可以在这部分代码运行后被使用。其他的声明方式不会提升。
  4. 由于没有外部环境,所以为null

在函数上下文中也类似:

  1. 会找到所有本函数中var声明函数声明let const class声明块级作用域声明的变量和函数
  2. 对标识符或者说是名字的重复进行处理。
  3. 登记环境记录的步骤跟全局执行类似,只不过换成了函数内部的声明。
  4. 记录外部环境的引用。

伪代码如下:

GlobalExectionContext = {  // 全局执行上下文
  LexicalEnvironment: {       // 词法环境
    EnvironmentRecord: {        // 环境记录
      Type: "Object",              // 全局环境
      // ...
      // 标识符绑定在这里 
    },
    outer: null            // 对外部环境的引用
  }  
}
  
FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: {       // 词法环境
    EnvironmentRecord: {        // 环境记录
      Type: "Declarative",         // 函数环境
      // ...
      // 标识符绑定在这里             // 对外部环境的引用
    },
    outer: {} //<Global or outer function environment reference>  
  }  
}

作用域和执行上下文的关系

MDN中,可以发现,二者其实是一个含义,只不过称呼不同,之前我也困惑了许久,下面也将使用作用域去代指执行上下文,如果还有疑问,可以在浏览器JavaScript代码执行中打个断点,在开发者工具中右侧区域可以找到scope这一栏,也侧面验证了这一点。
在这里插入图片描述

所以:

  • 全局作用域就是全局执行上下文
  • 函数作用域就是函数执行上下文
  • 块级作用域呢?块级作用域比较特殊,它没有this,可以认为它只存在语法环境,保存这标识及其引用关系。

作用域链

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

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

function foo(){
    console.log(a)
}

function bar(){
    var a = 3;
    foo();
}
var a = 1;
bar();

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

顺便在看下this的,感受下其中的不同,当然不想看的可以跳过

function foo(){
    console.log(this.a)
}

function bar(){
    var a = 2
    foo();
}
var a = 1;

bar() // 1
bar.call({a:3}); //1

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

变量提升

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

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

varlet的声明提升:

console.log(a) //undefined
var a = 1

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

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

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

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

console.log(age);
var age = 20
console.log(age);
//提升到最前面
function age() {
}
// 这样不会
//var age = function(){
//}
console.log(age);
//f age(){}
// 20
// 20
@wqhui wqhui mentioned this issue Apr 18, 2022
@kinch-tech
Copy link

kinch-tech commented Jun 27, 2023

在ES2021 Spec中,LexicalEnvironment/VariableEnvironment已经被修改成EnvironmentRecord了,就是把[[OuterEnv]]提取到EnvironmentRecord中,简化了层次关系

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants