realms
在执行ECMAScript代码之前,所有ECMAScript代码都必须与一个领域相关联。从概念上讲,一个领域由一组内部对象,一个ECMAScript全局环境,在该全局环境作用域内加载的所有ECMAScript代码以及其他相关的状态和资源组成。通俗点讲领域就是老大哥,在领域下的小弟都必须等大哥把事情干完才能做。领域被表示为领域记录(Realm Record)
[[Intrinsics]]: 内部方法
[[GlobalObject]]: 全局对象,宿主环境提供
[[GlobalEnv]]: Lexical Environment
agent(代理)
代理包括一ECMAScript执行上下文集合、一个执行上下文堆栈、一个正在运行的执行上下文、一个代理记录和一个正在执行的线程。除执行线程外,代理的组成部分仅属于该代理。
一个代理的执行线程独立于其他代理在代理的执行上下文中执行一个作业,除非一个执行线程可以被多个代理用作执行线程,前提是共享该线程的所有代理都没有一个代理记录的[[CanBlock]]属性为真。
分类概括
执行环境 = 词法环境 / 变量环境 / …
执行环境分类 = 全局 / 函数 / eval
词法环境分类 = 全局 / 函数 / 模块
词法环境 = ER + outer + this
ER分类 = declarative(DER) + object(OER)
全局ER = DER + OER
函数的 ER = thisValue / thisBindingStatus/ homeObject / newTarget
执行上下文
定义
JavaScript标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文”。
execution context 包含:
- lexical environment:词法环境,当获取变量或者this值时使用。
- variable environment:变量环境,当声明变量时使用
- code evaluation state:用于恢复代码执行位置。
- Function:执行的任务是函数时使用,表示正在被执行的函数。
- ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
- Realm:使用的基础库和内置对象实例。
- Generator:仅生成器上下文有这个属性,表示当前生成器。
与ES3的区别
如果你了解ES5版本的有关执行上下文的内容,会感到奇怪为啥有关VO、AO、作用域、作用域链等内容没有在本文中提及。其实两者概念并不冲突,一个是ES3规范中的定义,而词法环境则是ES6规范的定义。不同时期,不同称呼。
ES3 —> 最新
作用域 —> 词法环境+变量环境
作用域链 —> outer引用
VO|AO —> 环境记录
种类
- 全局执行上下文:当运行代码是处于全局作用域内,则会生成全局执行上下文,这也是程序最基础的执行上下文。
- 函数执行上下文:当调用函数时,都会为函数调用创建一个新的执行上下文。
- eval执行上下文:eval函数执行时,会生成专属它的上下文,因eval很少使用,故不作讨论。
执行栈
执行栈(Execution Context Stack)也称为调用栈,是用来管理执行期间创建的所有执行上下文的数据结构,栈顶元素就是正在执行的上下文。
程序开始运行时,会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内。
执行上下文的创建
这里只说两种
创建词法环境LexicalEnvironment;
创建变量环境VariableEnvironment;
在全局执行上下文中,this 指向 globalThis
词法环境(LexicalEnvironment)
词法环境的组成部分
- 环境记录EnvironmentRecord:存放变量和函数声明的地方;
- 外层引用outer:提供了访问父词法环境的引用,可能为null;
- this绑定ThisBinding:确定当前环境中this的指向;
环境记录ER
代码中声明的变量和函数都会存放在EnvironmentRecord中等待执行时访问。
环境记录EnvironmentRecord也有两个不同类型,分别为declarative和object。declarative是较为常见的类型,通常函数声明、变量声明都会生成这种类型的ER。object类型可以由with语句触发的,而with使用场景很少,一般开发者很少用到。
如果你在函数体中遇到诸如var const let class module import 函数声明,那么环境记录就是declarative类型的。
对象环境记录(Object Environment Record)
每个对象环境记录都与一个对象联系在一起,这个对象被称为绑定对象(binding object)。with用到了 objectER
var withObject={
a:1,
foo:function(){
console.log(this.a);
}
}
with(withObject){
a=a+1;
foo(); //2
}
声明性环境记录(Declarative Environment Record)
词法环境的类型
- 全局环境(GlobalEnvironment):在JavaScript代码运行伊始,宿主(浏览器、NodeJs等)会事先初始化全局环境,在全局环境的EnvironmentRecord中会绑定内置的全局对象(Infinity等)或全局函数(eval、parseInt等),其他声明的全局变量或函数也会存储在全局词法环境中。全局环境的outer引用为null。
这里提及的全局对象就有我们熟悉的所有内置对象,如Math、Object、Array等构造函数,以及Infinity等全局变量。全局函数则包含了eval、parseInt等函数。 - 模块环境(ModuleEnvironment):你若写过NodeJs程序就会很熟悉这个环境,在模块环境中你可以读取到export、module等变量,这些变量都是记录在模块环境的ER中。模块环境的outer引用指向全局环境。
- 函数环境(FunctionEnvironment):每一次调用函数时都会产生函数环境,在函数环境中会涉及this的绑定或super的调用。在ER中也会记录该函数的length和arguments属性。函数环境的outer引用指向调起该函数的父环境。在函数体内声明的变量或函数则记录在函数环境中。
全局上下文的ER
有一点特殊,因为它是object ER与declarative ER的混合体。在object ER中存放的是全局对象函数、function函数声明、async、generator、var关键词变量。在declarative ER则存放其他方式声明的变量,如let const class等。由于标准中将object类型的ER视作基准ER,因此这里我们仍将全局ER的类型视作object。
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
type: 'object', // 混合 object + declarative
NaN,
parseInt,
Object,
myFunc,
a,
b,
...
},
outer: null,
this: <globalObject>
}
}
LexicalEnvironment只存储函数声明和let/const声明的变量,与下文的VariableEnvironment有所区别。
比如,我们有如下代码:
let a = 10;
function foo(){
let b = 20
console.log(a, b)
}
foo()
// 它们的词法环境伪码如下:
GlobalEnvironment: {
EnvironmentRecord: {
type: 'object',
a: <uninitialized>,
foo: <func>
},
outer: <null>,
this: <globalObject>
}
FunctionEnvironment: {
EnvironmentRecord: {
type: 'declarative',
arguments: {length: 0},
b: <uninitialized>,
},
outer: <GlobalEnvironment>,
this: <globalObject> // 严格模式下为undefined
}
函数环境记录(Function Environment Record)
函数环境记录是一个声明性环境记录,
内部属性 | Value | 说明 | 补充 |
---|---|---|---|
[[ThisValue]] | Any | 函数内调用this时引用的地址,我们常说的函数this绑定就是给这个内部属性赋值 | |
[[ThisBindingStatus]] | “lexical” / “initialized” / “uninitialized” | 若等于lexical,则为箭头函数,意味着this是空的;强行new箭头函数会报错TypeError错误 | |
FunctionObject | Object | 在这个对象中有两个属性[[Call]]和[[Construct]],它们都是函数,如何赋值取决于如何调用函数 | 正常的函数调用赋值[[Call]],而通过new或super调用函数则赋值[[Construct]] |
[[HomeObject]] | Object / undefined | 如果该函数(非箭头函数)有super属性(子类),则[[HomeObject]]指向父类构造函数 | 若你写过extends就知道我在说什么 |
[[NewTarget]] | Object / undefined | 如果是通过[[Construct]]方式调用的函数,那么[[NewTarget]]非空 | 在函数中可以通过new.target读取到这个内部属性。以此来判断函数是否通过new来调用的 |
箭头函数的[[ThisBindingStatus]]是“lexical”,thisValue是null,如果使用了 this 会根据词法环境进行查找,本地没有就向外部词法环境中查找this值,不断向外查找,直到查到this值,
var a = 'global.a';
var obj1 = {
a:'obj1.a',
foo: function(){
console.log(this.a);
}
}
var obj2 = {
a:'obj2.a',
arrow:()=>{
console.log(this.a);
}
}
obj1.foo() //obj1.a
obj2.arrow() //global.a不是obj2.a
obj1.foo.bind(obj2)() //obj2.a
obj2.arrow.bind(obj1)() //global.a 强制绑定对ArrowFunction没有作用
模块环境记录(Module Environment Records)
模块环境记录是一个声明性环境记录,
变量环境(VariableEnvironment)
在ES6前,声明变量都是通过var关键词声明的,在ES6中则提倡使用let和const来声明变量,为了兼容var的写法,于是使用变量环境来存储var声明的变量。
var关键词有个特性,会让变量提升,而通过let/const声明的变量则不会提升。为了区分这两种情况,就用不同的词法环境去区分。
变量环境本质上仍是词法环境,但它只存储var声明的变量,这样在初始化变量时可以赋值为undefined。
有了这些概念,一个完整的执行上下文应该是什么样子的呢?来点例子🌰:
let a = 10;
const b = 20;
var sum;
function add(e, f){
var d = 40;
return d + e + f
}
let utils = {
add
}
sum = utils.add(a, b)
完整的执行上下文如下所示:
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
type: 'object',
add: <function>,
a: <uninitialized>,
b: <uninitialized>,
utils: <uninitialized>,
},
outer: null,
this: <globalObject>
},
VariableEnvironment: {
EnvironmentRecord: {
type: 'object',
sum: undefined
},
outer: null,
this: <globalObject>
},
}
// 当运行到函数add时才会创建函数执行上下文
FunctionExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
type: 'declarative',
arguments: {0: 10, 1: 20, length: 2},
[[ThisValue]]: <utils>,
[[NewTarget]]: undefined,
...
},
outer: <GlobalLexicalEnvironment>,
this: <utils>
},
VariableEnvironment: {
EnvironmentRecord: {
type: 'declarative',
d: undefined
},
outer: <GlobalLexicalEnvironment>,
this: <utils>
},
}
执行上下文创建后,进入到执行环节,变量在执行过程中赋值、读取、再赋值等。直至程序运行结束。
我们注意到,在执行上下文创建时,变量a``b都是的,而sum则被初始化为undefined。这就是为什么你可以在声明之前访问var定义的变量(变量提升),而访问let/const定义的变量就会报引用错误的原因。
let/const 与 var
存放位置
从上一结中,我们知道了let/const声明的变量是归属于LexicalEnvironment,而var声明的变量归属于VariableEnvironment。
初始化(词法阶段)
let/const在初始化时会被置为标志位,在没有执行到let xxx 或 let xxx = ???(赋值行)的具体行时,提前读取变量会报ReferenceError的错误。(这个特性又叫暂时性死区)
var在初始化时先被赋值为undefined,即使没有执行到赋值行,仍可以读取var变量(undefined)。
块环境记录(块作用域)
在ECMA标准中提到,当遇到Block或CaseBlock时,将会新建一个环境记录,在块中声明的let/const变量、函数、类都存放这个新的环境记录中,这些变量与块强绑定,在块外界则无法读取这些声明的变量。这个特性就是我们熟悉的块作用域。
什么是Block?
被花括号({})括起来的就是块。
在Block中的let/const变量仅在块中有效,块外界无法读取到块内变量。var变量不受此限制。
var不管在哪,都会变量提升~
函数声明和函数表达式声明
console.log(aa)
var aa = function (){console.log(1)}
console.log(aa)
function aa(){console.log(2)}
aa()
ƒ aa(){console.log(2)}
ƒ (){console.log(1)}
1
- 函数声明只是在预编译的时候放到词法环境的环境记录里面
- var a= b;这种会被分成两段
- 同名情况下,函数申明的优先级比 var 定义高
预编译的时候
- 预编译
- 先把 aa 变量的声明和赋值分开,把声明放到变量环境的环境记录里面是 undefined
- 遇到 aa 的函数声明,在词法环境加入 aa 的函数声明
- 执行代码
- 打印ƒ aa(){console.log(2)}
- 执行 aa= function (){console.log(1)}
- 打印ƒ (){console.log(1)}
查找顺序
沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
结论:
- 函数声明
定义在词法环境,由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
如果变量对象已经存在相同名称的属性,则完全替换这个属性
- 变量声明(var)
定义在变量环境,由名称和对应值(undefined)组成一个变量对象的属性被创建;
如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
- 变量声明(let和 const)
定义在词法环境