- 本文主要是介绍js执行过程中的执行环境相关内容,不涉及V8相关知识。
- 一、基本概念
- 当Javascript代码执行的时候会将不同的变量存储到内存的不同位置:堆(heap)栈(stack)中来进行区分。其中堆存放着一些对象;栈中存放着一些基础类型变量以及对象的指针。我们常说的执行栈和上面所说的栈的意义有些不同。
- Javascript在执行可执行脚本时,首先会创建一个全局执行上下文(globalContext),每当执行一个函数调用时都会创建一个可执行上下文(execution context)EC。当然可执行程序可能会存在很多函数调用,那么就会创建很多EC,所以Javascript引擎创建了执行上下文栈(Execution context stack)ECS来进行管理执行上下文。当前函数执行完成后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境中,这个过程是反复执行的,直到执行上下文栈中的代码全部执行完成。
- 二、执行上下文栈
- 1、执行上下文栈压栈顺序
- 一开始执行代码时,就会确定一个全局执行上下文(global execution context)作为栈底的默认值,这个全局执行上下文是永远不会被执行上下文栈弹出的。如果在全局环境中,调用了其他函数,就会重新创建一个新的EC,然后将这个可执行上下文推入执行上下文栈顶。
- 如果函数内调用了其他函数,相同的步骤就会发生:创建一个新的可执行上下文(EC),将新EC推入执行上下文栈顶。一旦一个可执行上下文(EC)执行完成,就会从执行上下文栈中推出(pop)。ESP指针负责EC出栈的指向问题。
- 备注:被压入到执行上下文栈中的全局执行上下文,它的实质就是全局对象(GO)。创建全局对象时,会将一些Javascript一些对象作为它的属性;并且会将window对象进行挂载,并将window对象的指向自身。
- 当多个函数存在,并且在函数内调用其他函数的执行上下文栈的压栈过程:
- 我们知道执行上下文栈中存放着全局上下文,全局上下文中存放着全局对象。那么可执行上下文(EC)被推入执行上下文栈中时,可执行上下文(EC)中存放的就是活动对象(AO)和变量对象(VO),实质上函数执行被推入执行上下文栈中的是当前函数的活动对象(AO)和变量对象(VO)。
- 活动对象(AO)管理着函数内部的变量;变量对象(VO)负责承接外部的联系,这样就形成了作用域链。
- 三、变量对象
- 四、活动对象
- 五、总结
- 1、粗略总结:
- 2、变量对象VO
- 3、活动对象AO
- 4、例子
- 六、最后
本文主要是介绍js执行过程中的执行环境相关内容,不涉及V8相关知识。
一、基本概念
当Javascript代码执行的时候会将不同的变量存储到内存的不同位置:堆(heap)栈(stack)中来进行区分。其中堆存放着一些对象;栈中存放着一些基础类型变量以及对象的指针。我们常说的执行栈和上面所说的栈的意义有些不同。
Javascript在执行可执行脚本时,首先会创建一个全局执行上下文(globalContext),每当执行一个函数调用时都会创建一个可执行上下文(execution context)EC。当然可执行程序可能会存在很多函数调用,那么就会创建很多EC,所以Javascript引擎创建了执行上下文栈(Execution context stack)ECS来进行管理执行上下文。当前函数执行完成后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境中,这个过程是反复执行的,直到执行上下文栈中的代码全部执行完成。
- 执行上下文栈:ECS
- 全局对象:GO(global Context)
- 活动对象:AO
- 变量对象:VO
- 全局执行上下文:GC
- 可执行上下文:EC
函数执行栈:callback stack,这个就是执行栈ECS的可视化
二、执行上下文栈
浏览器解释器执行js程序是单线程的,这就意味着同一时间只能有一个事情在进行。其他的活动和事件只能排队等候,生成一个等候队列,这就是执行栈(ESC)。
1、执行上下文栈压栈顺序
一开始执行代码时,就会确定一个全局执行上下文(global execution context)作为栈底的默认值,这个全局执行上下文是永远不会被执行上下文栈弹出的。如果在全局环境中,调用了其他函数,就会重新创建一个新的EC,然后将这个可执行上下文推入执行上下文栈顶。
如果函数内调用了其他函数,相同的步骤就会发生:创建一个新的可执行上下文(EC),将新EC推入执行上下文栈顶。一旦一个可执行上下文(EC)执行完成,就会从执行上下文栈中推出(pop)。ESP指针负责EC出栈的指向问题。
ECSstack = [ // 执行上下文栈globalContext // 全局上下文]
备注:被压入到执行上下文栈中的全局执行上下文,它的实质就是全局对象(GO)。创建全局对象时,会将一些Javascript一些对象作为它的属性;并且会将window对象进行挂载,并将window对象的指向自身。
当多个函数存在,并且在函数内调用其他函数的执行上下文栈的压栈过程:
function fun3() {console.log('fun3');}function fun2() {fun3();}function fun1() {fun2();}fun1();// 执行上下文栈的结果如下ECSstack = [fun3,fun2,fun1,globalContext]
我们知道执行上下文栈中存放着全局上下文,全局上下文中存放着全局对象。那么可执行上下文(EC)被推入执行上下文栈中时,可执行上下文(EC)中存放的就是活动对象(AO)和变量对象(VO),实质上函数执行被推入执行上下文栈中的是当前函数的活动对象(AO)和变量对象(VO)。
活动对象(AO)管理着函数内部的变量;变量对象(VO)负责承接外部的联系,这样就形成了作用域链。
三、变量对象
变量对象(Variable Object)VO是与执行上下文相关的特殊对象,它用来存储当前上下文的函数声明、函数的行参和变量。
通过一下例子来理解一下变量对象:
var a = 10;function test(x) {var b = 20;}test(30);// 全局执行上下文的变量对象(VO)VO===AO(global execution context) {a: 10,test: <reference to function>}// test函数的执行上下文中的变量对象(VO)VO(test functionContext) {x: undefined,b: undefined}
从上面的例子中可以得到:
只有全局执行上下文和可执行上下文(EC)存在变量对象(VO)
-
四、活动对象
在函数可执行上下文中,变量对象(VO)表示为活动对象(AO),当函数被调用时,这个特殊对象被创建。它包含普通参数与特殊参数对象。活动对象在函数上下文中作为变量对象使用。
function test(a, b) {var c = 22;function d() {}var e = function _e() {}(function() {});}test(10);AO(test) = {arguments: {0: 10,1: undefined,length: 2},a: 10,b: undefined,c: 22,d: pointer to funciton(),e: pointer to funciton()}
通过以上例子可知:
闭包函数是不会挂载到当前可执行上下文的AO或者VO上;
- 在函数可执行上下文中,VO是不能被访问的,此时有活动对象AO来扮演VO的角色;
AO包含:Arguments对象、内部定义的函数、内部定义的变量、绑定上对应的环境变量。
五、总结
以下总结可能和上方的VO和AO解释不同,因为VO是一个不可访问的,所以第三节变量对象中的例子是最后执行完成的样子,实质上就是AO的表现形式。
1、粗略总结:
Javascript引擎发现有代码调用了一个函数
- 在执行这个函数之前会创建一个可执行上下文EC
- 要执行的函数进入创建阶段,就是创建当前要执行函数的变量对象(VO)阶段(创建阶段)
- 将当前可执行上下文压入执行上下文栈的栈顶
-
2、变量对象VO
在Javascript引擎发现调用了一个函数,会经历创建可执行上下文的过程,创建当前执行函数的变量对象的过程,并通过scope属性指向外层的变量对象VO(其实就是通过变量对象VO指向了外层对象的活动对象AO),通过这样的指向就形成了作用域链。
在创建变量对象VO时,会把所有的变量的声明放到一个对象属性上,但是它们的属性值为undefined,这就是变量提升的过程。简单来说,变量对象就是存储变量的,然后本代码和子代码在执行的时候能够知道变量的值。并且变量对象VO中创建顺序:参数、变量、函数。
3、活动对象AO
活动对象AO可以理解为变量对象VO的实例,变量对象VO为活动对象AO提供了所有变量对象的模版。
变量对象VO和活动对象AO的关系:
在创建阶段会创建变量对象VO,VO是不能被访问的,但是可以访问活动对象AO的成员
- 变量对象VO和活动对象AO其实是一个东西,只是处在不同的可执行上下文的声明周期。VO是在创建阶段,AO是在执行阶段(就是被放入执行上下文栈的栈顶时)。粗粗暴理解就是当前可执行上下文位于执行上下文栈的栈顶的时候VO就会转换成AO
AO实际上包含VO的。因为除了VO的属性外,还包含arguments这个特殊的对象。活动对象AO在执行阶段被执行,处理实例化(激活)变量对象VO之外,还确定函数的arguments对象和逐行执行,将变量进行赋值
4、例子
根据以下代码进行分析:
var a = 1;function A() {var c = 3;function B() {var b = 2;}B();}A();
在执行以上代码时,首先会初始化全局环境:
初始化全局环境,确定全局执行上下文
初始化全局的变量对象VO和活动对象AO,由于全局对象只有一个,所以VO和AO一致都是GO(全局对象)
当执行A函数时
初始化创建当前可执行上下文
- 创建当前变量对象VO,然后初始化作用域链
- 执行阶段,根据变量对象VO创建活动对象AO,并确定arguments对象等
{AExecutionContext: {AO: {argument: {length: 0},c: 3},VO: {....},this: { 运行时确定 }Scope: [ 当前的AO, 外层的AO(就是GO)]},VO===AO===GO===globalContext: {a: 1,A: <Func>,this: window,Scope: window}}// 以上就是描述一个执行上下文栈的形态// 如果A函数中找不到要使用的变量就会沿着scope找到全局执行上下文GC中(就是全局对象)// 要注意的是作用域链是词法作用域,和调用关系无关,通过词法关系产生// 也可以说就是通过外层函数的变量对象VO来产生的
六、最后
当一个异步代码执行时就会引入事件队列这个概念。当js引擎遇到异步事件后,其实不是一直等待着异步事件返回,而是将异步事件挂起。等到异步事件执行完毕后,会放入事件队列中(注意:此时只是异步事件执行完成,其中的回调函数并没有去执行)。当执行队列执行完毕,主线程处于闲置状态时,会去事件队列中抽取最先被推入队列中的异步事件的回调,并放入执行上下文栈中,并执行同步代码。如此反复,这样就形成了一个无限循环。这个过程被称为事件循环。
