JS 执行上下文
执行上下文是评估和执行JS代码的环境的抽象概念。
每当JS代码在运行的时候,它都是在执行上下文中运行。
类型
- 全局执行上下文
- 任何不在函数内部的代码都在全局上下文中
- 创建一个全局的window对象(浏览器执行环境下),并且设置
this的值等于这个全局对象 - 一个程序中只会有一个全局执行上下文
- 函数执行上下文
- 每
当一个函数被调用时,都会为该函数创建一个新的上下文 - 每个函数都有自己的执行上下文,不过是在函数被调用时创建的
- 函数上下文可以有任意多个
- 每当一个新的执行上下文被创建,它会按定义的顺序执行
- 每
- Eval 函数执行上下文
- 在
eval函数内部的代码会有属于自己的执行上下文 eval()的参数是一个字符串- 如果字符串表示的是表达式,
eval()会对表达式进行求值。 - 如果参数表示一个或多个 JavaScript 语句,那么
eval()就会执行这些语句 - 如果
eval()的参数不是字符串,eval()会将参数原封不动地返回
eval(new String("2 + 2")); // 返回了包含"2 + 2"的字符串对象: String {"2 + 2"} eval("2 + 2"); // returns 4- 如果间接的使用
eval(),比如通过一个引用来调用它,而不是直接的调用eval。 从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。这就意味着,例如,下面的代码的作用声明创建一个全局函数,并且eval中的这些代码在执行期间不能在被调用的作用域中访问局部变量
function test() { var x = 2, y = 4; console.log(eval('x + y')); // 直接调用,使用本地作用域,结果是 6 var geval = eval; // 等价于在全局作用域调用 console.log(geval('x + y')); // 间接调用,使用全局作用域,throws ReferenceError 因为`x`未定义 (0, eval)('x + y'); // 另一个间接调用的例子 }- 它破坏代码结构,不利于阅读,而且在其中运行的代码没办法调试
- 会有性能问题,在旧的浏览器中如果使用了eval,性能会下降10倍。
eval()通常比其他替代方法更慢,因为它必须调用 JS 解释器,而许多其他结构则可被现代 JS 引擎进行优化。- 在现代浏览器中有两种编译模式:fast path和slow path。fast path是编译那些稳定和可预测(stable and predictable)的代码。而明显的,eval不可预测,所以将会使用slow path ,所以会慢。还有一个是,在使用类似于Closure Compiler等压缩(混淆)代码时,使用eval会报错
- 现代JavaScript解释器将javascript转换为机器代码。 这意味着任何变量命名的概念都会被删除。 因此,任意一个eval的使用都会强制浏览器进行冗长的变量名称查找,以确定变量在机器代码中的位置并设置其值。 另外,新内容将会通过
eval()引进给变量, 比如更改该变量的类型,因此会强制浏览器重新执行所有已经生成的机器代码以进行补偿。 但是,存在一个非常好的eval替代方法:只需使用 window.Function。 这有个例子方便你了解如何将eval()的使用转变为Function()。 - MDN eval
- 在
执行栈(调用栈)LIFO
执行栈(ECStack = [])用来管理执行上下文。
- 当 JavaScript 引擎第一次遇到脚本时,会创建一个全局的执行上下文并且压入当前执行栈;
ECStack.push(globalContext)
- 当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部;
ECStack.push(<functionName> functionContext)
- 引擎会执行栈顶的函数,该函数执行结束之后,执行上下文会从栈中弹出,控制流程到达当前栈中的下一个上下文。
ECStack.pop()
思考题
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
两段代码执行结果一样,但是执行上下文栈的变化是不同的。 第一段代码:
ECStack.push(globalContext)
ECStack.push(<checkscope> functionContext)
ECStack.push(<f> functionContext)
ECStack.pop()
ECStack.pop()
ECStack.pop()
第二段代码:
ECStack.push(globalContext)
ECStack.push(<checkscope> functionContext)
ECStack.pop()
ECStack.push(<f> functionContext)
ECStack.pop()
ECStack.pop()
作用域链
函数的作用域在函数定义的时候就决定了
函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
function foo() {
function bar() {
...
}
}
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
]
bar.[[scope]] = [
fooContext.VO,
globalContext.VO
]
创建执行上下文
创建执行上下文分2个阶段:
- 创建阶段
- 执行阶段
1. 创建阶段
JS代码执行前,执行上下文将经历创建阶段,创建阶段做3件事情:
- 绑定
this值 - 创建
词法环境 - 创建
变量环境
执行上下文在概念上表示如下:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
绑定 this
| 上下文分类 | this 指向 |
|---|---|
| 全局执行上下文 | 全局对象(浏览器中 this 引用 Window 对象) |
| 函数执行上下文 | 取决于该函数是如何被调用的: 1. 如果它被一个引用对象调用,this 会被 设置成那个对象 2. 其他情况会被 设置为全局对象或者 undefined(在严格模式下) |
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被对象 'foo' 调用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 对象,因为
// 没有指定引用对象
词法环境
- 它是一种规范类型;
- 简单的讲它是一种持有标识符-变量映射的结构;
- 标识符指的是变量/函数的名字;
- 变量是对实际对象[包含函数类型对象]或原始数据的引用。
它有两个组件:
- 环境记录器 存储变量和函数声明的实际位置
- 外部环境的引用 它可以访问其父级词法环境(作用域)
它有两种类型:
- 全局环境 没有外部环境引用的词法环境,外部环境引用是 null。它拥有内建的 Object/Array 等、在环境记录器内的原型函数(关联全局对象、比如 Window 对象)还有任何用户定义的全局变量和方法,
this指向全局对象 - 函数环境 内部定义的变量存储在环境记录器中,引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
词法环境伪代码:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
}
outer: <null>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
}
outer: <Global or outer function environment reference>
}
}
变量环境
它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。
在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
执行上下文看起来像这样:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
2. 执行阶段
在此阶段,完成对所有这些变量的分配,最后执行代码。
注意 — 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined。
总结
- 创建词法环境和变量环境可以用变量对象和作用域链来表示。
- 词法环境和变量环境中的 outer 就是用来与父元素建立连接的,最终形成作用域链的。
- 函数上下文中的词法环境和变量环境最终产出的其实是变量对象(VO/AO)
- 创建阶段
- 函数的所有形参
- 函数的声明
- 变量的声明
此时的 AO 是:function foo(a) { var b = 2; function c() {} var d = function() {}; b = 3; } foo(1);AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined }- 执行阶段:会顺序执行代码,根据代码,修改变量对象的值
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, c: reference to function c(){}, d: reference to FunctionExpression "d" } - 创建阶段
- 遇到同名的变量声明和函数声明时,则变量声明不会干扰已经存在的这类属性。
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
会打印函数,而不是 undefined 。
思考题
题目1
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
- 作用域链
foo.[[scope]] = [
globalContext.VO
]
- 执行 foo() 时执行栈入栈
ECStack.push(globalContext)
ECStack.push(<foo> functionContext)
- 创建阶段的 AO
AO = {
arguments: {
length: 0
}
}
- 执行阶段时顺序执行代码
- 遇到 console.log 时发现没有 a ,此时去作用域链查找,发现没有变量 a 的定义,报错
Uncaught ReferenceError: a is not defined
题目2
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
- 作用域链
bar.[[scope]] = [
globalContext.VO
]
- 执行全局代码时
- 2.1 入栈
ECStack.push(globalContext)
此时的ECStack:
ECStack = [
globalContext
]
- 2.2 代码执行时全局的 AO:
global = {
bar: reference to function bar(){}
}
- 执行 bar
- 3.1 入栈
ECStack.push(<bar> functionContext)
此时的ECStack:
ECStack = [
barContext,
globalContext
]
- 3.2 创建阶段的AO
AO = {
arguments: {
length: 0
}
}
-
3.3 执行阶段
- 执行 a = 1 时,当前AO没有变量a,作用域链继续查找,一直查询到全局,把 a 放到了全局,并赋值全局的 a = 1
global = { a: 1, bar: reference to function bar(){} }- 遇到 console.log 时发现AO上没有 a ,向上查找,发现全局有 a,此时 a 为1 ,所以打印 1
-
3.4 bar 执行完后从执行栈中出栈
ECStack.pop()
此时的ECStack:
ECStack = [
globalContext
]
-
- 代码执行完后
ECStack.pop()
此时的ECStack为空
ECStack = []
题目3
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
- 作用域链
checkscope.[[scope]] = [
globalContext.VO
]
- 执行代码
- 准备阶段
globalContext.AO = {
scope: undefined,
checkscope: reference to function checkscope(){}
}
- 执行阶段
globalContext.AO = {
scope: "global scope",
checkscope: reference to function checkscope(){}
}
- 执行 checkscope
- 3.1 准备阶段
-
- 第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = { Scope: checkscope.[[scope]], }-
- 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]], }-
- 第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] } -
- 3.2 执行,修改AO的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
- 3.3 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];