也来谈谈JS的执行上下文与词法环境

前言

JS 的执行上下文(execution context)和词法环境(Lexical environment)是很多前端er时不时听到,却不曾深入了解的知识点。似乎只具理论意义,也不会影响我们日常编程。事实上,词法环境的原理与我们常常用到的很多知识点息息相关,比如变量提升,闭包(closure),作用域链(scope),箭头函数的原理等等。这些看似散乱甚至很多同学需要背诵的知识点,其实根本上都是JS词法规则的外化与延伸。

本文的目的就是通过了解JS 的词法环境,让其像一条项链一样,将一些散乱无序的知识点串联起来,同时对这些知识点能有更深的理解。

执行上下文(Execution context )概念

本节关键词: execution context, execution context stack(执行栈),running execution context

ECMAScript规范 中第8章叫做8 Executable Code and Execution Contexts. 其中 8.3 Execution Contexts 有关于 Execution context 的描述:
在这里插入图片描述
执行上下文( execution context)是程序的运行时环境,分为以下几种:

  1. Global execution context : JS 是单线程执行,在一个执行宿主中(浏览器或V8) 只有一个全局执行上下文
  2. Function execution context
  3. Eval execution context

函数运行时会创建一个执行栈(execution context stack),作用就是维护(track) 执行上下文。
我们通过一段简单的代码图示一下:

在这里插入图片描述
在这里插入图片描述

执行上下文的建立和执行

创建/编译 阶段

执行上下文的建立阶段完成了词法环境的创建this 的绑定

词法环境( Lexical environment ) 的创建:

关键词: Lexical Environments 包括 LexicalEnvironment(词法环境) 和 VariableEnvironment(变量环境)
在这里插入图片描述
从文档描述可以看出,词法环境和变量环境是执行上下文的一部分。

执行上下文(Execution context) 还包括code evaluation state,Realm 等其他要素,本文不涉及

我们还以上一节的代码为例,图示一下执行上下文创建阶段完成后的内部结构:

注意: 在执行上下文的创建阶段,LexicalEnvironment 值只是 VariableEnvironment 的拷贝
在这里插入图片描述

闭包

从上图我们可以发现,在词法环境中有一个指向外部词法环境的指针 outer,全局上下文词法环境中的outer 为 null。这其实就是所谓的闭包:

函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)

在此阶段生成的指向外部词法环境的 outer 指针,会一直伴随着整个函数的声明周期,在执行阶段依然发挥作用

这正是下面代码运行结果为 ‘global’ 的原因。bar 的词法环境是在运行前建立的,bar 函数内部没有关于 x 变量的声明,因此会沿作用域链(outer)向上查找。

var x = 'global';

function bar() {
    console.log('x is ' + x);
}

function foo() {
   var x = 'foo';
   bar();
}
// 运行结果: 
// x is golbal
变量提升

还结合上图,在执行上下文创建阶段,主要完成如下几项工作:

  1. 函数上下文环境参数的绑定(arguments)
  2. 函数表达式提升(hoist)
  3. 变量的声明,并将其初始值设置为 undefined (hoist)

#2 和 #3涉及到的其实就是我们经常遇到的 hoist 即 , 函数及变量的声明都将被提升到所在context的最顶部。这也是为什么代码中第4行在执行时会打印出 undefined 的原因。

注意: 我们在例子中只用了 var 来讨论变量提升。事实上 ES6 推出的 let 和 const 也有变量提升,但是与 var 相比,多了暂时性死区的概念。具体可参考这篇博文 let/const 的变量提升与暂时性死区

执行阶段

在执行上下文的创建阶段,完成了变量声明,但是其值为 undefined. 在代码的执行阶段,才会完成对变量真正的赋值。这个赋值过程也被称为 Identifier Resolution

作用域链 (scope chain)

从上图可以看出作用域链的轨迹,从当前的词法环境通过 outer 寻找上一级的词法环境直到全局为止,如果未找到相应变量,会报 Reference Error

  if (lexicalEnvironment == null) {
    throw ReferenceError(identifier + " is not defined");
  }

作用域 (Scope)

词法环境(Lexical)与作用域(Scope)

MDN The current context of execution. The context in which values and expressions are “visible” or can be referenced

从 MDN 的定义可以看出,所谓作用域(scope)其实就是执行上下文,只不过是scope这个词,主要体现执行上下文中变量和表达式的可达性

作用域链 在上一节也提到了,即词法环境中指向执行外部执行上下文的指针形成的链条。

执行上下文,词法环境,作用域链之间的关系用一个不太严谨的图示如下:
在这里插入图片描述

静态(Static)作用域与动态(Dynamic)作用域

ES6 提供了两种作用域。静态作用域即变量,属性方法等归属于哪个对象,在编译时就已经确定。反之,动态作用域 即变量,属性的归属只有在程序运行阶段才可以确定。

ES6 中动态作用域的例子就是 with 和 eval 关键字。下面是一个栗子:

var x = 10;
 
var o = {x: 30};
var storage = {};
 
(function foo(flag) {
 
  if (flag == 2) {
    eval("var x = 20;");
  }
 
  if (flag == 3) {
    storage = o;
  }
 
  with (storage) {
 	// x 的值在编译阶段无法判断,可能来自全局, eval, 或者 storage 的任何一个地方
    alert(x); //
 
  }
 
  // organize recursion on 3 calls
 
  if (flag < 3) {
    foo(++flag);
  }
 
})(1);

ES6 推出的严格模式(strict) 可以增强代码安全性,很重要的一个体现就是只允许静态作用域,禁止动态作用域(with, eval).

箭头函数的词法环境

MDN 中关于箭头函数的描述是这样的:

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

this 与 arguments 的问题

为什么箭头函数没有自己的 this 和 arguments ?

道理其实很简单,对箭头函数而言,在执行上下文的创建阶段,没有做 thisBinding 和 建立相应的 arguments 对象。因此箭头函数要获取 this 只能通过作用域链获取最近词法环境的 this,也就是定义箭头函数的执行上下文的 this.

在这里插入图片描述

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值