# JS执行过程
# 执行堆栈
当Javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。
JS在执行可执行的脚本时,首先会创建一个全局可执行上下文的 globalContext,每当执行到一个函数调用时,都会创建一个可执行上下文 (execution contex) EC. 程序执行可能会存在很多函数调用,那么就会创建很多 EC, 所以 JavaScript 引擎创建了执行上下文栈 (Execution context stack,ECS) 来管理执行上下文。 当函数调用完成, js 会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境 ... 这个过程反复执行,直到执行栈中的代码全部执行完毕。
# 执行栈
浏览器解释器执行 JS 是单线程的过程,这就意味着同一时间,只能有一个事情在进行。 其他的活动和事件只能排队等候,生成出一个等候队列执行栈 (Execution Stack)
# 执行栈压栈顺序
一开始执行代码的时候,变确定了一个全局执行上下文 global execution context 作为默认值。 如果在你的全局环境中,调用了其他的函数,程序将会再创建一个新的 EC,然后将此 EC 推入到栈中 execution stack
如果函数内再调用其他函数,相同的步骤将会再次发生:创建一个新的EC->把EC推入执行栈。一旦一个EC执行完成,变成从执行栈中推出 (pop). ESP 指针负责ES出栈指向
ECStack=[
globalContext
]
function f1() {
f2();
console.log(1);
};
function f2() {
f3();
console.log(2);
};
function f3() {
console.log(3);
};
f1();//3 2 1
# 变量对象 (Variable Object)
变量对象 VO 是与执行上下文相关的特殊对象,用来存储上下文的函数声明,函数 形参和变量
var a = 10
function test(x){
var b = 20
}
test(30)
那么他们在VO中是如何表示的呢
// 全局上下文的变量对象
VO(globalContext) = {
a: 10,
test: <reference to function>
};
// test函数上下文的变量对象
VO(test functionContext) = {
x: 30,
b: 20
};
VO分为 全局上下文的变量对象VO,函数上下文的变量对象VO
VO(globalContext) === global
# 活动对象 (Activation Object)
在函数上下文中,变量对象被表示为活动对象 AO,当函数被调用后,这个特殊的活动对象就被创建了。它包含普通参数与特殊参数对象 (具有索引数学的参数映射表)。活动对象在函数上下文中作为变量对象使用,预编译在此阶段发生
- 在函数执行上下文中,VO 是不能直接访问的,此时由活动对象扮演VO的角色
- Arguments对象它包括如下属性:callee 、length
- 内部定义的函数
- 绑定上对应的变量环境
- 内部定义的变量
VO(functionContext) === AO
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10)
当进入带有参数 10 的test函数上下文时, AO 表现如下:
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
AO里并不包含函数“x”。这是因为“x” 是一个函数表达式(FunctionExpression, 缩写为 FE) 而不是 函数声明,函数表达式不会影响VO
活动对象和变量对象的区别:
变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。
当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。
# 深度活动对象
深度活动对象 Activation Object 分为 创建阶段 和 执行阶段,
# 创建阶段
很明显,这个时候还没有执行代码。 此时的变量对象会包括:
函数的所有形参 (only函数上下文):没有实参,属性值设为 undefined
函数声明:如果变量对象已经存在相同名称的属性,则完全替换这个属性。
变量声明:如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性
function foo(i) {
var a = 'hello';
var b = function privateB() { };
function c() {
}
}
foo(22);
我们在执行 foo(22)的时候,EC 创建节点会类似生成下面这样的对象:
fooExecutionContext = {
scopeChain: { Scope },
AO: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
VO:{..}
Scope: [AO, globalContext.VO],
}
形参 arguments 这时候已经有赋值了,但是变量还是 undefined,只是初始化的值
# 执行阶段
这个阶段会顺序执行代码,修改变量对象的值,代码一行一行的被解释执行
fooExecutionContext = {
scopeChain: { ... },
AO: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
VO:{..}
Scope: [AO, globalContext.VO],
this: { 运行时确认 }
}
# 补充活动对象
var x = 10; function foo() {
var barFn = Function('alert(x); alert(y);');
barFn(); // 10, "y" is not defined
}
foo();
通过函构造函数创建的函数的[[scope]]属性总是唯一的全局对象(LexicalEnvironment)。
Eval code - eval 函数包含的代码块也有同样的效果
# 小结
VO 函数是上下文的链接 AO 是函数自身的
ECStack = [
fun3
fun2,
fun1,
globalContext
];
VO分为 全局上下文的变量对象VO,函数上下文的变量对象VO
VO(globalContext) === global
VO(fooExecutionContext) === function
# 异步代码
当一个异步代码(如发送ajax请求数据)执行后会如何呢?这需要了解另一个概率就是:事件队列(Task Queue)
当js引擎遇到一个异步事件后,其实不会说一直等到异步事件的返回,而是先将异步事件进行挂起.等到异步事件执行完毕后,会被加入到事件队列中。(注意,此时只是异步事件执行完成,其中的回调函数并没有去执行。)当执行队列执行完毕,主线程处于闲置状态,会去异步队列那抽取最先被推入队列中的异步事件,放入执行栈中,执行其中的回调同步代码。 反复如此,这样就形成了一个无限的循环。这就是这个过程被称为 事件循环(Event Loop) 的原因。
function test() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i
}
}
return result
}
let r = test()
r.forEach(fn => {
console.log('fn',fn())
})
- 函数test执行完出栈,留下 AO(test)有个i的指向
- 函数在执行的时候,函数的执行环境才会生成. 所以fn 执行的时候生成作用域链条指向如下
AO(result[i]) --> AO(fn) --> VO(G) 加个闭包就立⻢创建了执行环境
那么其实一切也就迎刃而解了。闭包的原理是 Scope (堆空间中存储closure(foo)), this 的原理是动态绑定,作用域链 的原理是Scope: [AO, globalContext.VO], eval 不能回收的原理是推不进 AO ,变量提升的原理是AO的准备阶段,异步 队列的原理是 ECS.
# ES5+
JS执行上下文的创建阶段主要负责三件事:
确定this
创建词法环境 (LexicalEnvironment)
创建变量环境 (VariableEnvironment)
创建过程如下:
ExecutionContext = {
// 确定this的值
ThisBinding = <this value>,
// 创建词法环境
LexicalEnvironment = {},
// 创建变量环境
VariableEnvironment = {},
};
# 确定 this
官方的称呼为 This Binding,在全局执行上下文中,this 总是指向全局对象,例如浏览器环境下 this 指向 window 对象。而在 nodejs 中指向这个文件的 module 对象。
在函数执行上下文中,this的值取决于函数的调用方式,this 的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new绑定、箭头函数
# 词法环境
词法环境有两部分组成:
词法记录:用于存储变量和函数声明的实际位置
对外部环境引入记录:用于保存它可以访问的其它外部环境 (有点作用域链的意思)
前面提到全局执行上下文和函数执行上下文,所以导致了词法环境也分两种:
全局词法环境
是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量 this 的值指向这个全局对象
// 全局执行上下文
GlobalExectionContext = {
// 词法环境
LexicalEnvironment: {
// 环境记录
EnvironmentRecord: {
// 全局环境
Type: "Object",
// ... 标识符绑定在这里
// 对外部环境的引用
outer: <null>
}
}
函数词法环境
用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。
// 函数执行上下文
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
// // 函数环境
Type: "Declarative",
// ... 标识符绑定在这里
// 对外部环境的引用
outer: <Global or outer function environment reference>
}
}
# 变量环境
为了继续去适配早期的JS的var等,新的规范增加了变量环境(VariableEnvironment)。变量环境也可以说是词法环境,它具备词法环境所有的属性。其环境记录器包含了由变量声明语句
在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>
}
}
注意: 只有在遇到函数multiply调用时才会创建函数执行上下文
在执行上下文创建阶段,函数声明 与 var 声明的变量在创建阶段已经被赋予了一个值, var 声明被设置为了 undefined,函数被设置为了自身函数,而 let const 被设置为 uninitialized(未初始化)
这就是为什么你可以在声明之前访问 var 定义的变量(尽管是 undefined),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因,这就是所谓的 变量提升