执行上下文和作用域
本文最后更新于:1 年前
执行上下文和作用域
执行上下文
是一个描述代码运行时所在环境的抽象概念,JavaScript 引擎再开始执行代码前,会创建全局执行上下文 ,全局代码(不属于任何函数的代码)在全局执行上下文中执行。 全局执行上下文 在每个 JS 程序中只有一个。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中(execution stack)。在函数执行完后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript 程序中的执行流正是由这个便利的机制控制着。
执行上下文
因为 JavaScript 是个单线程语言,这意味着同一时间只能执行一个任务,而 JavaScript 内部原理又会经过分词(tokenize)
、预解析(preparse)
、解析(parse)
,这里不展开讲。解析执行的时候有三种情况生成执行上下文(execution context):全局代码、函数代码,eval 代码。
在 ES3 中,执行上下文中包含:
作用域链(scope)
变量对象(variable object)
this
在 ES5 中,变成了包含:
词法环境(lexical environment)
(作用域)变量环境(variable environment)
this
可参考ECMA262 规范。
再后来(ES8),this 值被归入了词法环境中:
词法环境,包含this,获取变量时使用(lexical environment)
(作用域)变量环境,声明变量时使用(variable environment)
代码执行状态(code evaluation state)
正在执行的函数(Function)
正在被执行的代码(ScriptOrModule)
内置对象实例(Realm)
当前生成器,只有生成器上下文才有(Generator)
VO 和 AO
变量对象(variable object)
的属性由变量和函数声明构成,每一个执行上下文都会被分配一个变量对象,而在函数上下文情况下,参数列表也会被加入到变量对象中。
活动对象(activation object)
会在函数被激活时就会被创建并分配给执行上下文。
在函数执行上下文中,VO 是不能直接访问的,而 AO 是可以访问的。
可以用以下代码举例说明:
function outerFun(arg1, arg2) {
var value1 = 1;
var value2 = 2;
function func1() {
var innerV1 = 3;
var innerV2 = 4;
return '1';
}
function func2() {
return '2';
}
function value2() {
return '3';
}
}
outerFun();
他的变量对象创建是这样的:
key | value | 备注 |
---|---|---|
arguments | Arguments Object | 检查当前执行上下文的参数列表,创建 arguments 对象 |
func1 | <func1() reference> | 检查到 function 声明,属性值指向函数所在的内存地址 |
func2 | <func2() reference> | |
value1 | undefined | 检查到执行上下文的所有 var 声明 并建立属性 |
value2 | undefined |
词法环境和变量环境
变量环境(variable environment)
:用于记录 var,function 声明的绑定。词法环境(lexical environment)
:用于记录其他声明的绑定(如 let、const、class 等)。
而词法环境中又包含两个组成部分:
环境记录(environment record)
:记录相应代码块的标识符绑定,可理解为代码块内变量、函数等都绑定于此,对应 ES3 中的 VO(变量对象)和 AO(活动对象)。外部词法环境引用(outer environment)
:用于形成多个词法环境在逻辑上的嵌套结构,以实现可以访问外部词法环境变量的能力,对应 ES3 中的作用域链。
环境记录包含了:
声明式环境记录(Declarative Environment Record)
:定义直接将标识符绑定的元素,例如 let、const、class、module、import 等对象式环境记录(Object Environment Record)
:用于记录与某些对象属性绑定的元素,例如 with 语句、var 声明和函数声明全局环境记录(Global Environment Record)
:就是存放 globalThis 上的属性元素
可以用如下代码大概解释是怎么回事:
let a = 1;
const b = 2;
var c = 3;
function test(d, e) {
var f = 10;
return f * d * e;
}
c = test(a, b);
简要地抽象成看得懂的结构的话,预解析阶段的全局环境内:
GlobalLexicalEnvironment = {
LexicalEnvironment: {
OuterReference: null,
EnvironmentRecord: {
a: <uninitialized> ,
b: <uninitialized>,
},
},
VariableEnvironment: {
EnvironmentRecord: {
test: <func reference>,
c: undefined,
}
}
}
执行到最后时的环境:
GlobalLexicalEnvironment = {
LexicalEnvironment: {
OuterReference: null,
EnvironmentRecord: {
a: 1 ,
b: 2 ,
},
},
VariableEnvironment: {
EnvironmentRecord: {
test: <func reference>,
c: 20,
}
}
}
FunctionLexicalEnvironment = {,
LexicalEnvironment: {
OuterReference: <GlobalLexicalEnvironment>,
EnvironmentRecord: {
arguments: {0: 1, 1: 2, length: 2},
},
},
VariableEnvironment: {
EnvironmentRecord: {
f:10,
}
}
}
为什么要有两个环境
主要是为了实现块级作用域的同时不影响var声明和函数声明
。因为在 ES6 之前并没有块级作用域的概念,而 ES6 后又可以使用 let 和 const 实现,那这时候就需要不影响 var 声明和函数声明了。
原理是当正在执行的上下文,运行到块级代码时:
console.log(foo); // 输出:undefined
{
function foo() {
console.log('hello');
}
}
console.log(foo); // 输出: ƒ foo() {console.log('hello')}
会将此时的词法环境记录下来,并且在块级内部创建新的词法环境,然后将外部词法环境指向上一层被记录的词法环境中。然后块级中的 let 和 const 都会被记录到当前的词法环境,而 var 和函数声明就还是绑定在变量环境上了。
作用域
作用域指的源代码中定义变量的区域,它规定了执行代码对变量的访问权限。而常见的作用域有词法作用域(lexical scope)
和动态作用域(dynamic scope)
。而 Javascript 使用的是词法作用域,也就是说无论函数在哪里被调用,也无论它如何被调用,它的作用域都只由函数被声明时所处的位置决定
词法作用域和动态作用域
词法作用域即定义时就决定作用域了,而动态作用域即在执行调用时才能确定作用域。
在《JavaScript 权威指南》中的例子就是个典型的作用域问题:
var scope = 'global scope';
function checkscope() {
var scope = 'local scope';
function f() {
return scope;
}
return f();
}
checkscope();
// local scope
var scope = 'global scope';
function checkscope() {
var scope = 'local scope';
function f() {
return scope;
}
return f;
}
checkscope()();
// local scope
上述两个例子执行结果都会返回 local scope
,因为 JavaScript 采用词法作用域,而函数 f
在 checkscope
内部被定义,所以此时已经确定了他所在的作用域了。
可能会好奇什么是动态作用域,可以将如下代码保存,并 bash 执行,因为 bash 是动态作用域:
c=1;
function func1 () {
echo $c;
}
function func2() {
local c=2;
func1;
}
func2
这里会输出 2,而在 JavaScript 中会打印 1:
var c = 1;
function func1() {
console.log(c);
}
function func2() {
var c = 2;
func1();
}
func2();
这里就是体现词法作用域和动态作用域的区别。
闭包
在计算机术语中,函数体作用域内包含了其变量环境,这个函数就是闭包(clourse),通俗点就是拥有自由变量
并处在这些自由变量
外部且能访问到外部的这些自由变量
的函数。
很拗口是吧?其实严格意义上 JavaScript 中所有函数都是闭包
。为了实现词法作用域,JavaScript 函数对象的内部状态不仅包含逻辑代码,还包含了当前作用域链的引用。
就很简单的例子:
var a = 1;
function func() {
console.log(a);
}
func();
func 方法内拥有不属于自身变量环境的自由变量 a,那么 func 就是闭包。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!