Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入js之深究ES6规范前后的执行上下文相关 #5

Open
FE-Sadhu opened this issue Apr 18, 2019 · 0 comments
Open

深入js之深究ES6规范前后的执行上下文相关 #5

FE-Sadhu opened this issue Apr 18, 2019 · 0 comments
Labels
深入js笔记 about javascript

Comments

@FE-Sadhu
Copy link
Owner

此文结构是ES5规范前和后的执行上下文相关知识分开介绍。如果对ES5之前的比较熟悉了,可以跳过。想复习一下或不熟悉的也可以看一看。(看了网上很多相关记录的, 其实我觉得基本都众说纷纭,很多种说法,而且有些知识点已经不是以前书中所记录的样子了,包括某些著名书籍如第三版高程/y-d-k-js。当然我这仅仅只是站在chrome浏览器的角度上说的这话,我的理解以chrome的调试为准。)在参考过不少国内外资料也包括部分规范后,我决定写几篇文章记录下,于我而言算为之后秋招复习,更想知道各位对这篇文章有不同的有“证据”,符合逻辑的分析、看法。

ES5规范前

执行上下文是什么?

执行上下文的定义是: 当前js代码被解析和执行时所处环境的抽象概念。

可以理解为,当前js代码的运行环境。

再来了解下js的运行环境,有三种:

  • 全局环境(当js代码运行起来时,会首先进入该环境)
  • 函数环境(当函数被调用执行时,会进入当前函数中执行代码)
  • eval(不建议使用)

每进入一个不同的运行环境就会创建一个相应的执行上下文(Execution Context),容易知道一个js程序中一般会创建多个执行上下文,并且js引擎会以栈的方式对这些执行上下文进行处理,形成函数调用栈(call stack)

一张图理解函数调用栈(call stack)

简单分析下,这个图里例子的函数调用栈的出栈入栈情况。

1. 首先,js引擎进入全局环境,创建全局执行上下文Global Context(图中是用main()来代表)并入栈。
2. 代码执行到console.log()函数,创建该函数的函数执行上下文,入栈。
4. 接着调用bar(6)函数,创建bar函数执行上下文,入栈。
5. 执行到调用foo(...),创建foo函数执行上下文,入栈。
6. foo函数的函数体执行完了,返回该函数返回值,出栈。
7. bar函数的函数体执行完了,返回该函数返回值,出栈。
8. console.log()函数执行完,返回值到console上,出栈。
9. 若此时关闭了浏览器,则全局执行上下文Global Context出栈。

总结:

  1. 全局环境的EC在代码运行起来时创建并入栈,一定永远处于call stack栈底,并且在关闭浏览器时出栈。
  2. 函数执行上下文在函数被调用时创建并入栈,在函数体代码执行结束时出栈,等待垃圾回收。

再来个例子测试下理解了没?

var color = 'blue';

function changeColor() {
    var anotherColor = 'red';

    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }

    swapColors();
}

changeColor();

思考一下再看答案:

执行上下文的生命周期

知道了执行上下文和函数调用栈是啥,结合起来是怎么利用的。接下来,就来分析下执行上下文的生命周期。

执行上下文的生命周期有两个阶段:

  • 创建阶段

    此阶段主要做了三件事情:

    1. 创建变量对象Variable Object。
    2. 建立作用域链Scope Chain。(此时的作用域链应为[VO, [[scope]],具体区别稍后会说。)
    3. 绑定this。(因为this的指向取决于函数的调用方式)

  • 代码执行阶段

    此阶段会完成变量赋值,函数引用,以及执行其他代码。

用张完整的图来解释执行上下文的生命周期就是:

了解了执行上下文的生命周期后,接下来我们从创建阶段开始一步步说。

创建阶段

我们在介绍生命周期那里解释了创建阶段会做的三件事情,现在从创建变量对象开始说起。

创建变量对象

一、对于函数执行上下文而言

创建变量对象(Variable Object),前辈们基本都是总结出了三个过程:

  1. 创建arguments对象,检查当前上下文中的参数,建立该对象的属性与属性值,并且把形参初始化在VO中(不理解的话看以下例子),仅在函数环境(非箭头函数)中进行,全局环境没有此过程。
  2. 检查当前上下文的函数声明,按代码顺序查找,将找到的函数提前声明,如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则为指向该函数所在堆内存地址的引用,如果存在,则会被新的引用覆盖。
  3. 检查当前上下文的变量声明,按代码顺序查找,将找到的变量提前声明,如果当前上下文的变量对象没有该变量名属性,则在该变量对象以变量名建立一个属性,属性值为undefined;如果存在,则忽略该变量声明。

注:

  1. 在全局环境中,window对象就是全局执行上下文的变量对象,所有的变量和函数都是window对象的属性方法。
  2. 所以函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明。

面试里所谓的变量提升,其实创建变量对象的三个规则里已经说明了。

二、对于全局执行上下文而言,

以浏览器为例,全局对象为window。

全局上下文有一个特殊的地方,它的变量对象,就是window,所有的变量和函数都是window对象的属性方法。而这个特殊,在this指向上也同样适用,this也是指向window。

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

利用以上规则,我们举个例子来分析创建阶段的执行上下文EC

function fun(a, b) {
    var num = 1;
    var foo = function () {};
    function test() {
        console.log(num)
    };
    var test = 2;
}

fun(2, 3)
// 以浏览器为例
// 全局执行上下文 windowEC
windowEC = {
    VO: window,
    scopeChain: [],
    this: window
}

// 函数执行上下文 funEC
funEC = {
    VO: {
        arguments: {
            0: 2,
            1: 3,
            length: 2
        },
        a: undefined, // 形参初始化在VO中
        b: undefined,
        test: <test reference>, // 表示test函数所在堆内存地址的引用
        num: undefined, // 变量声明提升
        foo: undefined
    },
    scopeChain: [...], // 暂时不管作用域链,稍后解释。
    this: window
}

看懂了吗? 举个例子再试试?会的可以跳过。

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

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

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

test();

注:上面我们解释了创建变量对象Variable Object的过程,但需要注意,此时仅仅只是执行上下文生命周期中的创建阶段,尚未进入执行阶段,此时的VO中的属性是不能访问的。怎样才能访问呢?接下来让执行阶段的操作来揭秘。

创建阶段的作用域链和this绑定暂时跳过,稍后介绍。

执行阶段

我们已经知道代码执行阶段会完成变量赋值函数引用,以及执行其他代码

说白了就是引擎开始执行代码了嘛。

举个例子:var a = 2,在上下文创建阶段扫描代码,变量提升'var a',可以理解为此时执行的是var a (实际并没有执行而只是扫描)。而在执行阶段等于引擎就执行的是a=2了。但是注意一点,a=2这个操作,实际是 作用域(规则)配合引擎进行的LHS查询,为VO中的a属性赋值2。那我们之前不是刚说过VO中的属性是不能访问的吗?这里怎么可以访问了?其实我们之前说的没错,只是到了执行阶段VO产生一些变化了:

进入执行阶段之后,变量对象(Variable Object)转变为了活动对象(Activi Object),里面的属性都能被访问了,然后开始进行执行阶段的操作。

这也就是我们思维导图里画的所谓的 VO => AO

变量对象VO与活动对象AO的区别

他们其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。

还是上面那个例子,给观众姥爷们感受一下执行阶段产生的影响(下面的例子也会把作用域链的值也写出来,用数组表示,不懂的可先感受下,暂时跳过):

function fun(a, b) {
    var num = 1;
    var foo = function () {};
    function test() {
        console.log(num)
    };
    var test = 2;
}

fun(2, 3)
// 函数上下文创建阶段: 
funEC = {
    VO = {
        arguments: {
            0: 2,
            1: 3,
            length: 2
        },
        a: undefined,
        b: undefined,
        test: <test reference>,
        num: undefined,
        foo: undefined
    },
    scopeChain: [VO, GO(window)], // 也可以写成 [VO, [[scope]]] 
    this: window
}
// 函数上下文执行阶段: 
// VO => AO
funEC = {
    AO = {
        arguments: {
            0: 2,
            1: 3,
            length: 2
        },
        a: 2,
        b: 3,
        test: 2,
        num: 1,
        foo: <foo reference>
    },
    scopeChain: [AO, GO(window)], // 也可以写成 [AO, [[scope]]]
    this: window
}

贴个图验证下:


局部变量指的是当前函数的局部变量。在函数执行上下文中,当前函数的局部变量、函数、形参都是声明保存在变量对象中的。

再来个例子,试着写一写?

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

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

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

test();

作用域链

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

我们知道函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。这里我们来说下作用域链:

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

结合一个例子来理解:

var a = 20;

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

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

    return innerTest();
}

test();

在上面的例子中,当执行到刚刚调用innerTest函数,进入innerTest函数环境,此时处于innerTest执行上下文的创建阶段。全局执行上下文和test函数执行上下文已进入执行阶段,所以他们的活动对象和变量对象分别是AO(global),AO(test)和VO(innerTest),而innerTest的作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,如下:

innerTestEC = {
    VO: {...},  // 变量对象
    scopeChain: [VO(innerTest), AO(test), AO(global)], // 作用域链
    this: window
}

我们这里直接使用数组表示作用域链,作用域链的活动对象或变量对象可以直接理解为作用域(但实际上作用域只是一套规则)。

  • 作用域链的第一项永远是当前作用域(当前上下文的变量对象或活动对象);

  • 最后一项永远是全局作用域(全局执行上下文的活动对象);

  • 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或函数,找到则会停止查找,找不到则一直查找到全局作用域,再找不到则会抛出引用错误。


(上图AO(global)。写AO(innerTest)表示此时是进入执行阶段时函数的作用域链)

验证一下?

注: 闭包可以使内部函数访问外层函数的局部变量,即访问外层函数环境的活动对象属性。

再来看看我们思维导图:

这里提到了[[scope]],而且为什么作用域链可以表示成 AO+[[scope]] 呢?

首先我们要知道[[scope]]是啥。

借用下汤姆大叔的解释:

  1. 理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是通过函数内部的[[scope]]属性来实现的。
  2. [[scope]]是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。
  3. [[scope]]在函数创建时被存储--静态(不变的),永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。

看不懂?没事儿,看下面张图(例子还是上一个,没有改变):

现在知道为什么作用域链还可以表示成 AO+[[scope]] 了吧?还没反应过来的再去仔细看看上上张图。

关于ES5规范前的执行上下文相关知识,暂且写到这里。有额外的知识会在之后的文章中再记录。

目前到此为止没有疑惑吗?

我有。

JS代码的整个执行过程不是分编译阶段(编译器完成,将代码翻译成可执行代码)和执行阶段(引擎完成,执行可执行代码)嘛。

下面阐述两个观点:

  1. 我们都知道函数执行上下文是在函数调用时创建的。那么都执行到调用函数这步了,引擎肯定已经开始执行可执行代码了。

  2. 再来,从创建变量对象过程中包含声明提升我们就知道这应该是在代码执行前进行的,也就是在编译阶段进行的!比如var a = 2var a代码被扫描,a变量提升,应该是在编译阶段进行的;a = 2则是在引擎在执行阶段执行。也就是说,创建变量对象过程应该发生在编译阶段,而网上也有文章如是说。这也是尚未进入上下文执行阶段,变量对象VO不能访问其中属性的一种解释。

这两个观点有错吗?如果没错,这两个观点矛盾吗?思考下。







我一度觉得矛盾,究矛盾根源就是:我认为JS代码整个执行过程会很直男地从左到右,过了编译阶段进入执行阶段,就永远不能再进编译阶段。而这就与上面两个观点相驳:已经在执行阶段进行到创建函数上下文了,怎么可能又再返回编译阶段呢?

带着疑问各处找资料参阅书籍的时候,最终在书上找到了答案(原话):

  1. JavaScript compilation doesn't happen in a build step ahead of time, as with other languages. // JS的编译过程不是发生在构建之前的。
  2. JS engines use all kinds of tricks (like JITs, which lazy compile and even hot re-compile, etc.) which are well beyond the "scope" of our discussion here. // js引擎会用尽各种方法(比如JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
  3. that any snippet of JavaScript has to be compiled before (usually right before!) it's executed. // 任何js代码片段在执行前都要进行编译(通常就在执行前)。

ES5规范后

推荐一篇比较权威的译文:【译】理解 Javascript 执行上下文和执行栈

思维导图:

关于这部分,我是之前做了些笔记在笔记本,我拍照贴上来,字不好看但还能看清吧...不想看就跳过哈~这笔记就是个小总结,没啥干货,东西都在那篇译文里。

聪明的你可以自行理解对比下es5前后规范执行上下文的不同与相同之处。

本文完。

希望看到各位技术人对这篇文章有不同的有“证据”,符合逻辑的分析、看法~

觉得写得还不错的,可以点个star支持下作者🖖🏼

@FE-Sadhu FE-Sadhu added the 深入js笔记 about javascript label Apr 18, 2019
@FE-Sadhu FE-Sadhu changed the title 深入JS笔记之深究ES5规范前后的执行上下文相关 深入js笔记之深究ES5规范前后的执行上下文相关 Apr 18, 2019
@FE-Sadhu FE-Sadhu changed the title 深入js笔记之深究ES5规范前后的执行上下文相关 深入js之深究ES5规范前后的执行上下文相关 Apr 21, 2019
@FE-Sadhu FE-Sadhu changed the title 深入js之深究ES5规范前后的执行上下文相关 深入js之深究ES6规范前后的执行上下文相关 Oct 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
深入js笔记 about javascript
Projects
None yet
Development

No branches or pull requests

1 participant