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

在说明执行上下文和作用域之前,我们需要了解一下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 由调用者确定箭头函数是词法决定

伪代码:

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

举个例子:

const foo = function(num){
    var a = "Hello";
    var b = function varB(){};
    function c(){}
    let d = "World"
}
foo(10);
  • 创建阶段
executionContextObj = {
    variableObject : {
      num: 1, //确定形参并且赋值
      arguments: {0:10, length:1}, //确定argumens 
      c: function c(){}, //确定字面变量定义的函数
      a: undefined,// var 定义的局部变量,初始值为undefined
      b: undefined,// var 定义的局部变量,初始值为undefined
      // let 定义的变量只会记录,到执行赋值的阶段这里为暂死区
    }, 
    scopeChain : {},//词法解释阶段已经确定了作用域,该阶段相当于引用
    this : {}
}
  • 执行阶段
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的,可以认为它只保存了词法环境,保存这标识及其引用关系。

作用域链

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

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

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);

// 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)

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值