JavaScript中Scope、Lexical Scope、Scope Chain、Lexical Environment、Execution Context

860 阅读7分钟

Scope(作用域)

Ths scope is space police that rules the accessibility variables

Scope(作用域)负责收集并维护声明的标识符,并制定了访问规则

简单的说就是scope规定了变量的可访问性,JavaScript引擎可以在作用域中根据标识符来进行变量查找,也就是将标识符与变量对应起来,同时,scope隔离了变量,使得不同scope中的变量可以同名但不同值,相互之间没有影响

function foo() {
  // "foo" function scope
  let count = 0;
  console.log(count); // logs 0
}

function bar() {
  // "bar" function scope
  let count = 1;
  console.log(count); // logs 1
}

foo();
bar();

如上代码所示,foobar属于两个不同的作用域,都定义了相同的变量count,但是保存了不同的值,彼此之间没有影响

Scope nesting

function outerFunc() {	// outer scope
	let outerValue = 'I am outer scope'
  
  function innerFunc() {	// inner scope
    console.log(outerValue)	// log → 'I am outer scope'
  }
  innerFunc()
}
outerFunc()

上面代码由outerFunc和innerFunc形成了两个作用域,在inner scope嵌套在outer scope中,inner scope可以访问outer scope中的变量,由此得出以下规则:

  • 作用域可以嵌套
  • 外部作用域可以访问内部作用域中的变量

Lexical scope(词法作用域)

在传统的编译语言的流程中,源代码在执行之前会经历三个步骤,统称为编译,

  1. 分词/词法分析(Tokenizing/Lexing)

    在这个过程中,由字符组成的字符串会被分解成有意义的代码块,这些代码块被称为词法单元(token)。如:var a = 2; 会被分解成:var、a、=、2、; 。空格是否会被当成词法单元,由空格在编程语言中的意义所决定

  2. 解析/语法分析(Parsing)

    这个过程会将词法单元流(数组)转换成一个由元素逐级嵌套组成的代表程序语法结构的树,这个树被称为**"抽象语法树"(Abstract Syntax Tree, AST)**。

  3. 代码生成

    将AST转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等信息息息相关。

    简单地说就是有某种方法可以将var a = 2;的AST转换为一组机器指令,用来创建一个叫做a的变量,并将一个指存储在a中

    —— 《你不知道的JavaScript》上卷 P5

作用域有两种类型:

  • 词法作用域(或称静态作用域)
  • 动态作用域

词法作用域: 其作用域在词法阶段(lexical stage)确定,不是在执行阶段(excution stage),因此,词法作用域与代码的书写位置有很大关系,也可以说词法作用域是由代码的书写位置来决定的

动态作用域: 其作用域在代码执行阶段才确定

JavaScript引擎使用的是词法作用域

在JavaScript中有几种作用域?

  • global scope(全局作用域)
  • function scope(函数作用域)
  • block scope(块级作用域):需要注意的是在ES6之前并不存在block scope(var声明变量时不会形成块级作用域),ES6添加了letconst等关键字,在它们所声明的代码块中会形成块级作用域

Scope chain(作用域链)

The Scope Chain is the hierarchy of scopes that will be searched in order to find a function or variable.

由于作用域是可以嵌套的,当JavaScript引擎在当前作用域没有找到使用的变量,那么会向上级作用域继续查找,直到最顶级的作用域——Global Scope,如果在Global Scope中也没有找到,那么会将该变量添加到window对象上(浏览器环境,且为非strict 模式下),这种像链条一样,不断向上层作用域查找的过程,可以称为作用域链

注意:这种不断向上层作用域查找的过程其实是通过Lexical Environment中outer来实现的

Lexical Environment(词法环境)

是一个保存identifier-variable mapping标识符-变量映射关系的数据结构,identifier指的是变量名/函数名,variable指的是变量的引用(包括object和function)或基本类型的值)的数据结构

一个Lexical Environment主要包含两部分:

  • Environment Record:存储变量和函数声明的地方
    • declaration Environment Record
    • Object Environment Record
    • Global Environment Record
  • outer(Reference to the outer Environment):指向外部的Lexical Environment

注意当程序执行时,便会创建Lexical Environment

类似于下面这种结构:

lexicalEnvironment = {
  environmentRecord: {
    <identifier> : <value>,
    <identifier> : <value>
  }
  outer: < Reference to the parent lexical environment>
}

看一个实际例子:

let language = 'JS';
function a() {
  let b = 25;  
  console.log('Inside function a()');
}
a();
console.log('Inside global execution context');

上面代码执行时会有两个Lexical Environment,一个是Global Lexical Environment,一个是function a创建的Lexical Environment:

GlobalLexicalEnvironment = {
  environmentRecord: {
    language: 'JS',
    a: <reference to function object>
  },
  outer: null
}

注:Global Lexical Environment的outer是null,因为对于global scope(全局作用域)而言,其Lexical Environment为null

function a创建的Lexical Environment:

FunctionLexicalEnvironment = {
  environmentRecord: {
		b: 25
  },
  outer: <GlobalLexicalEnvironment>
}

function a的outer指向Global Lexical Environment,因为其代码是被包含在global scope内

Execution Context

JavaScript在执行函数全局(global)的代码时会创建相应的Execution Context(执行上下文):

  • 全局代码:Global Execution Context(全局上下文)
  • 函数:Function Execution Context

在创建执行Execution Context时,会相应的创建Lexical Environment(词法环境)

关于Lexical Scope、Execution Context和Lexical Environment三者的关系,我的理解是:后两者的创建其实会依赖lexical scope,因为lexical scope决定了标识符的可访问性,这又与创建Lexical Environment息息相关(与outer有关)

关于JavaScript引擎处理执行上下文的过程:

  1. 当JavaScript引擎执行全局作用域中的代码时,会创建一个Global Execution Context(全局上下文),并将其添加 Execution Context Stack(也称为:Call Stack-调用栈) 中,并将其设置为 Running Lexical Environment
    1. 在创建Global Execution Context时,也会创建Global Lexical Environment(全局词法环境),在词法环境中主要做两件事:实现Environment Record(实现标识符与变量的绑定) 和 outer指向(指向外层的词法环境,在Global Execution Context中,outer指向null)
  2. 当执行到函数时,会为当前函数创建Function Execution Context(函数上下文),并将其添加到Execution Context Stack中,将其设置为 running execution context
    1. 创建Function Execution Context与创建Global Execution Context时做的事情相同,只不过outer 指向的是Global Lexical Environment(PS:函数的嵌套调用,其实也体现在词法作用域的嵌套)
  3. 当函数执行完成后,会将当前函数的Function Execution Context 从Execution Context Stack中弹出并销毁,同时将Global Lexical Environment 设置为Running Lexical Environment

:以上过程只是考虑没有嵌套函数的情况,嵌套函数的情况类似

注意

  • 在ES5的specification与ES6的specification其实有很多不同,在上文中并未做区分

  • 上面描述的一些内容其实有所省略,例如:

    • Execution Context的组成,其实不仅仅只有Lexical Environment,还有Variable Environment、PrivateEnvironment,

    • Environment Record的类型,包括:Declaration Environment Record、Object Environment Record和Global Environment Record,同时,在标准中 明确说明Environment Record有[outer],用于指向外层的Environment Record,而在我们上面的描述中,[outer]字段是在Lexical Environment中的,其与Environment Record是同级的关系,包括网上的大多文章中,都是如此描述的,但与标准似乎不符,不知是否对标准理解有误? 答:这主要是因为 ECMA-262 5.1 EditionECMA-262 12th Edition 有一些区别,5.1的标准发布于2011年,12th是2021年最新的标准,在这10年间,标准中的规范有所变动导致实现也有所变动导致的,5.1中关于Lexical Environment的描述 ,12th中并未找到Lexical Environment的详细描述,只有关于Environment Record的描述

疑惑

  • Lexical Scope是在词法分析阶段被确定的,而Execution Context和Lexical Environment是在执行阶段创建的,是否Execution Context和Lexical Environment的创建依赖于Lexical Scope?从目前了解到的信息来看,似乎是的
  • 在ECMAScript262的规范中并未提及Scope和Lexical Scope 这些术语及其实现,是否因为scope的生成其实是代码编译执行过程中的一个必不可少的常规操作,所以,在规范中并未提及? 根据scope的概念,规范中与其最相关的是Lexical Environment,是否Lexical Environment就是对Scope的具体实现呢?

参考: