JavaScript学习之执行上下文

128 阅读8分钟

学习冴羽的JavaScript深入之执行上下文栈JavaScript深入之变量对象JavaScript深入之作用域链

前言

对于每个执行上下文都有三个重要的属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

\

\

执行上下文

每当控制器到了可执行代码的时候,就会进入一个执行上下文。执行上下文以理解当前代码的执行环境,它会形成一个作用域。

变量和函数的上下文决定它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object AO) 而这个上下文中定义的所有变量和函数都保存在这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。

上下文在其所有代码都执行完毕后被销毁,包括定义在它上面的所在的变量和函数(全局上下文会在程序退出之前销毁,关闭网页)


在js引擎并非是一行一行执行分析程序的,而是一段段地分析执行。当执行一段代码时,会进行一个准备工作(比如,变量提升,函数提升)

怎么才算一段呢?

可执行代码

  • 全局代码
  • 函数代码
  • eval代码

我们写的函数多了,怎么管理这些执行上下文呢?

执行上下文栈

Js引擎创建了执行上文栈(execution context stack, ECS)来管理执行上下文。

模拟一下:

ECStack = [];

JS引擎在解释执行代码时,最先碰到的是全局代码,所以先将全局执行上下文(globalContext)压入栈,并且这个globalContext会一直在栈底,直到所有代码执行完(程序执行完之后)才被清除。

ECStack = [
    globalContext
];

然后遇到了函数代码:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

函数代码会根据执行(调用)创建执行上下文,然后将这个函数的执行上下文压入栈中。函数执行完,这个函数的执行上下文就会从执行栈中弹出。

// 伪代码

// fun1()
ECStack.push(<fun1> functionContext);

// fun1中竟然调用了fun2,还要创建fun2的执行上下文
ECStack.push(<fun2> functionContext);

// fun2还调用了fun3!
ECStack.push(<fun3> functionContext);

// fun3执行完毕
ECStack.pop();

// fun2执行完毕
ECStack.pop();

// fun1执行完毕
ECStack.pop();

// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext

可执行上下文的生命周期

可执行上下文的生命周期分为两个阶段: 创建阶段 运行阶段

创建阶段

创建阶段执行上下文会分别创建变量对象、确认this指向、作用域链

执行阶段

这个阶段执行上下文会做具体执行的事,如变量赋值、函数引用

变量对象

变量对象创建会经历几个过程(按照顺序来):

  1. 建立arguments对象,检查当前上下文中的参数,建立该对象下的属性与属性值。
  2. 建检查当前上下文的函数声明(函数式声明,不是表达式的),在变量对象中以函数名建立一个属性。属性值为指向该函数所在内存地址的引用。如果函数名的属性已存在,那么该属性会被新的函数所在内存地址引用覆盖。
  1. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中建立为这个变量名的属性,属性值先为undefined,如果该变量在变量对象中存在,则覆盖,如果存在该属性对应的是一个同名函数,则该变量会被跳过,不覆盖函数(函数优先于变量)

关于 变量被跳过

console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
// 上述的执行顺序为

// 首先将所有函数声明放入变量对象中
function foo() { console.log('function foo') }

// 其次将所有变量声明放入变量对象中,但是因为foo已经存在同名函数,因此此时会跳过undefined的赋值
// var foo = undefined;

// 然后开始执行阶段代码的执行
console.log(foo); // function foo
foo = 20;

这就是变量和函数提升的规律,对于面试的时候可以用变量对象创建的过程来解释变量提升。

在上面我们看到了函数的声明优先级比变量声明的优先级高一些。

创建阶段(未进入执行阶段之前)变量对象中的属性是不能被访问的。

执行阶段

进入执行阶段之后,变量对象(VO)变成了活动对象(AO),这时里面的属性可以访问了。

此时代码会顺序执行,然后遇到哪句代码然后根据代码修改AO中的值(这个也是按照顺序的)。

康康创建阶段和执行阶段变量对象是啥样的

function test() {
    console.log(a);
    console.log(foo());

    var a = 1;
    function foo() {
        return 2;
    }
}

test();

创建阶段:

// 创建过程
testEC = {
    // 变量对象
    VO: {},
    scopeChain: {}
}

// 现在暂时不详细解释作用域链,所以把变量对象专门提出来说明

// VO 为 Variable Object的缩写,即变量对象
VO = {
    arguments: {...},  //注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,我做了这样的处理
    foo: <foo reference>  // 表示foo的地址引用
    a: undefined
}

这时VO中的属性不可以访问哦!

执行阶段:

// 执行阶段
VO ->  AO   // Active Object
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}

面试的时候问:活动对象与变量对象的区别?

答:他们其实是同一个对象,只是处于执行上下文的不同生命周期。不过处于栈顶的执行上下文(当前执行上下文)的变量对象才会成为活动变量。

所以上面demo1的执行顺序为:

function test() {
   function foo() {
        return 2;
    }
   

    var a ;
    console.log(a);
    console.log(foo());
  	a=1;
}

test();

再举个例子

// demo2
function test() {
    console.log(foo);
    console.log(bar);

    var foo = 'Hello';
    console.log(foo);
    var bar = function () {
        return 'world';
    }

    function foo() {
        return 'hello';
    }
}

test();

创建阶段:

// 只写VO,作用域链啥的不管
VO={
	arguments:{...},
   // 这里既是变量定义的代码在函数的前面,解析时还是函数优先,然后foo 已经有了,所以foo变量跳过
  foo: <foo reference>,
  // 只有函数式声明的函数才优先于变量,表达式型的和变量定义差不多,没有优先级。
  bar:undefined
}

执行阶段:

// 执行阶段,跟着代码顺序依次赋值
VO -> AO
VO = {
    arguments: {...},
    foo: 'Hello',
    bar: <bar reference>,
    this: Window
}

全局的变量对象

对于浏览器来说,全局的变量对象就是window对象。

在this指向上也同样适用,this也是指向window。

// 以浏览器中为例,全局对象为window
// 全局上下文
windowEC = {
    VO: Window,
    scopeChain: {},
    this: Window
}

全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如不关掉浏览器窗口,全局上下文就会一直存在。其他所有的上下文环境,都能直接访问全局上下文的属性。

作用域链

作用域

在JavaScript中,我们可以将作用域定义为一套规则,这套规则来管理引擎如何在当前作用域以及其子作用域中根据标识符名字(就是变量名和函数名)进行变量查询的

  • JavaScript中(在ES6之前)只有全局作用域和函数作用域
  • 作用域和执行上下文是不同概念。

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码编译成可执行代码,这个阶段作用域会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建的。

作用域链

我们知道在函数调用激活时,会创建对应的执行上下文,在执行上下文生成过程,变量对象、作用域链、this指向会分别被确定。

作用域链,是当前环境与上层环境的一系列变量对象组成的。他保证了当前执行环境对符合访问权限的变量和函数有序访问。

举个例子

var a = 20;

function test() {
    var b = a + 10;

    function innerTest() {
        var c = 10;
        return b + c;
    }

    return innerTest();
}

test();

这个例子的执行上下文创建的顺序 为: 全局上下文、test函数上下文、innerTest函数上下文。我们设他们的变量对象为VO(global)、VO(test)、VO(innerTest)。每一层作用域链包含它上一层的作用域链。

我们看一下innerTest的作用域链:

innerTestEC={
  VO:{...},// 变量对象
  scopeChain:[VO(innerTest),VO(test),VO(global)]
}

在查找变量时,先从自己的变量对象中查找,没有的话上级(词法层的上级)的变量对象中找,直至找到global的变量对象,这样有多个变量对象构成的链表叫做作用域链。

因为在test中调用的innerTest,此时test的执行上下文还没结束。所以test的变量对象也是活动变量。