执行上下文(Execution Context)
每当 Javascript 代码在运行的时候,它都是在执行上下文中运行,用来跟踪记录代码运行时环境的抽象概念。
执行上下文的三种类型:
-
全局执行上下文
任何不在函数内部的代码都在全局环境中
一个程序只有一个全局执行上下文
全局环境会生成一个全局对象,在浏览器中为
window
,this
会指向该全局对象 -
函数执行上下文
每当一个函数被调用时, 都会为该函数创建一个新的函数执行上下文,并被推入 环境栈(执行栈) 中,当函数实行完毕出栈,控制权返回给之前的执行上下文(栈顶的执行上下文)。
-
Eval 函数执行上下文
执行上下文的创建
执行上下文在创建时,做了下面两件事:
-
创建词法环境(LexicalEnvironment)
-
创建变量环境(VariableEnvironment)
看网上说法不一,有说在创建时进行 this
的绑定,也有说 this
的绑定是在 创建词法环境时执行的。而通过查看ecma
原文可以看到结论:
The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context.
大意是 执行上下文 通过 词法环境 来确定 this
的绑定。
词法环境
词法环境是ECMA中的一个规范类型,基于代码词法嵌套结构用来记录标识符和具体变量或函数的关联。
词法环境内部有两个组件:
-
环境记录
EnvironmentRecord
:用来储存变量和函数声明 -
外部引用
outer
:提供访问父词法环境的能力
与执行上下文对应,词环境也有 全局环境(GlobalEnvironment) 和 函数环境(FunctionEnvironment) 的区分。
let a = 1;
function fn(){
let b = 2;
}
fn();
// 伪代码 全局词法环境
GlobalEnvironment: {
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录(存储变量和函数声明)
type: 'object',
a: <uninitialized>,
fn: <function>
},
outer: <null>, // 由于是全局词法环境,无外部引用,故为 null
this: <globalObject>
}
}
// 伪代码 函数词法环境
FunctionEnvironment: {
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录(存储变量和函数声明)
type: 'declarative',
arguments: {length: 0},
b: <uninitialized>,
},
outer: <GlobalEnvironment>, // 外部引用为父词法环境
this: <globalObject>
}
}
变量环境
变量环境本质上仍是词法环境,但它只存储var声明的变量,这样在初始化变量时可以赋值为undefined(变量提升)。
var a = 1;
let b = 2;
// 伪代码 全局词法环境
GlobalEnvironment: {
VariableEnvironment: { // 变量环境
EnvironmentRecord: { // 环境记录(存储变量和函数声明)
type: 'object',
a: <undefined>, // 注意这里为 undefined
},
outer: <null>, // 由于是全局词法环境,无外部引用,故为 null
this: <globalObject>
},
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录(存储变量和函数声明)
type: 'object',
b: <uninitialized>,
},
outer: <null>, // 由于是全局词法环境,无外部引用,故为 null
this: <globalObject>
}
}
执行栈
程序运行时,首先把全局环境入栈,后面每次遇到函数被执行时,将该函数的执行上下文入栈,函数执行结束则出栈,所有代码都执行完毕,最后全局环境出栈
console.log(m); // undefined
var m = 999;
var num = 1;
var fnA = function(param) {
var a = 2;
console.log('fnA', param)
fnB(a);
}
var fnB = function(param) {
var b = 3;
console.log('fnB', param);
}
fnA(4);
如上代码,首先全局执行上下文入栈,同时全局执行上下文初始化,这时候环境中的变量都是未定义的状态(所以用var定义的变量在遇到变量提升时会为 undefined)
执行栈 Stack:[全局执行上下文]
num: undefined
fnA: undefined
fnB: undefined
this: 浏览器中为 window,其余情况(比如node)为该全局对象(一个空对象)
然后在逐行执行代码的过程中,变量被赋值
num: 1
fnA: function
fnB: function
执行到 fnA(4)
时,fnA 函数被调用,创建一个函数执行上下文,并入栈
执行栈 Stack:[全局执行上下文, fnA的函数执行上下文]
a: undefined
param: 4
arguments: [4]
this: window
fnA
函数执行上下文中,代码逐行执行,对变量赋值,代码执行到 fnB(a)
时,创建 fnB
的函数执行上下文并入栈
执行栈 Stack:[全局执行上下文, fnA的函数执行上下文, fnB的函数执行上下文]
fnB
执行结束后 fnB的函数执行上下文 出栈
执行栈 Stack:[全局执行上下文, fnA的函数执行上下文]
fnA
执行结束后 fnA的函数执行上下文 出栈
执行栈 Stack:[全局执行上下文]
最后代码执行完毕,全局执行上下文 出栈,栈空
作用域
作用域 和 执行上下文并不是同一个概念。
JavaScript 属于解释型语言,JavaScript 的执行分为:解释 和 执行 两个阶段
解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定
执行阶段:
- 创建执行上下文
- 执行函数代码
- 垃圾回收
作用域 就是在 解释阶段 确定的,而 执行上下文 是在 执行阶段 创建的。
所以对于一个函数来说,函数的作用域在函数定义的时候就已经确定了;而函数的执行上下文在其被调用的时候才会创建,且每次执行的执行上下文可能不一样(this不一样、里面的变量值不一样)。
this
的值是在函数执行前,创建函数执行上下文时被赋值的,this
的值跟调用该方法的对象有关,当前执行上下文指向的对象,即谁调用就指向谁。
Tips:箭头函数除外,箭头函数中的
this
始终指向该函数定义时所在作用域指向的对象,由于作用域不变,所以箭头函数的this指向在定义时就固定了,不会改变。
如下代码,fn
的函数作用域在定义的时候确定了,而不同的调用则创建了不同的执行上下文:
const obj = { a: 1 };
const other = { b: 2 };
function fn(param) {
console.log('this: ', this, 'param: ', param);
}
obj.fn = fn;
obj.fn(1); // this: obj param: 1
fn(2); // this: 全局对象 param: 2
fn.call(other, 3) // this: other param: 3
作用域链
先看下面代码,全局作用域中有 变量a 和 函数fn;fn的函数作用域中有 变量b。
fn
中打印变量 a
,但 fn 的函数作用域中并没有 a
,但我们仍然可以使用 a
,这个变量 a
对函数作用域来说就是 自由变量。而向父级作用域取值这一特性就是借助了作用域链。
var a = 1;
function fn() {
var b = 2;
console.log(a, b); // 1, 2
}
fn();
再看一个例子,下面的代码最终打印的结果是 1
,因为对于 fn1
来说,他的父级作用域是全局作用域,而不是 fn2
var a = 1;
function fn1() {
console.log(a);
}
function fn2() {
var a = 2;
fn1();
}
fn2();
参考
https://juejin.cn/post/6844904145372053511#heading-3
https://juejin.cn/post/7043408377661095967#heading-2
https://blog.fundebug.com/2019/03/15/understand-javascript-scope/
https://262.ecma-international.org/6.0/#sec-resolvethisbinding