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();
如上代码所示,foo和bar属于两个不同的作用域,都定义了相同的变量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(词法作用域)
在传统的编译语言的流程中,源代码在执行之前会经历三个步骤,统称为编译,
分词/词法分析(Tokenizing/Lexing)
在这个过程中,由字符组成的字符串会被分解成有意义的代码块,这些代码块被称为词法单元(token)。如:
var a = 2;会被分解成:var、a、=、2、;。空格是否会被当成词法单元,由空格在编程语言中的意义所决定解析/语法分析(Parsing)
这个过程会将词法单元流(数组)转换成一个由元素逐级嵌套组成的代表程序语法结构的树,这个树被称为**"抽象语法树"(Abstract Syntax Tree, AST)**。
代码生成
将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添加了
let、const等关键字,在它们所声明的代码块中会形成块级作用域
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引擎处理执行上下文的过程:
- 当JavaScript引擎执行全局作用域中的代码时,会创建一个Global Execution Context(全局上下文),并将其添加 Execution Context Stack(也称为:Call Stack-调用栈) 中,并将其设置为 Running Lexical Environment
- 在创建Global Execution Context时,也会创建Global Lexical Environment(全局词法环境),在词法环境中主要做两件事:实现Environment Record(实现标识符与变量的绑定) 和 outer指向(指向外层的词法环境,在Global Execution Context中,outer指向null)
- 当执行到函数时,会为当前函数创建Function Execution Context(函数上下文),并将其添加到Execution Context Stack中,将其设置为 running execution context
- 创建Function Execution Context与创建Global Execution Context时做的事情相同,只不过outer 指向的是Global Lexical Environment(PS:函数的嵌套调用,其实也体现在词法作用域的嵌套)
- 当函数执行完成后,会将当前函数的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 Edition 与 ECMA-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的具体实现呢?
参考: