• js基础梳理究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?


    日常在群里讨论一些概念性的问题,比如变量提升,作用域和闭包相关问题的时候,经常会听一些大佬们给别人解释的时候说执行上下文,调用上下文巴拉巴拉,总有点似懂非懂,不明觉厉的感觉。今天,就对这两个概念梳理一下,加深对js基础核心的理解。

    1. 执行上下文(execution context)与可执行代码(execution code)

    1.1 首先说一下,可执行代码的类型有哪些:

    • 全局代码:例如加载外部的js文件或者本地标签内的代码。全局代码不包括 function 体内的代码
    • 函数代码:function体内的代码
    • eval代码:eval()函数计算某个字符串,并执行其中的js代码。比如eval("alert('hello world')")。虽然很强大,但实际用得很少,不讨论。

    当js引擎遇到这三种类型的代码的时候,都会进行一些准备工作,这些准备工作,专业的说法就叫执行上下文。或者说js引擎遇到这三种类型的代码的时候,就会进入到一个执行上下文。

    简而言之,执行上下文是评估和执行javascript代码的环境的抽象概念。每当javascript代码在运行的时候,它都是在执行上下文中运行。执行上下文可以理解为当前代码的执行环境,它会形成一个作用域╭(╯^╰)╮,作用域就作用域嘛,说得这么拗口,非要搞个什么执行上下文的概念)。

    1.2 那么js引擎在遇到可执行代码的时候,它究竟会做哪些准备工作呢?

    • 全局执行上下文:创建一个全局的window对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
    • 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
    • eval函数执行上下文:略

    其实没必要刻意去区分可执行代码与执行上下文。个人理解,当别人跟你聊一些概念性的东西,聊到可执行代码,可执行上下文,执行环境的时候,其实他们可能是想说作用域,只不过表述方式不同罢了。

    之前写的这个,有点问题,惭愧。作用域与执行上下文是完全不同的两个概念。

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

    2. 执行上下文栈(Execution context stack, ECS)

    在一个javascript程序中,必定会产生多个执行上下文,javascript引擎会以栈的方式来处理它们,也就是执行上下文栈(很多文章可能会称它为执行栈,执行上下文堆栈,函数调用栈,其实都是差不多的意思)。

    关于栈的概念和特性在上一篇博客:js基础梳理-内存空间已有介绍。

    为了模拟执行上下文栈的行为,可以把它定义为一个数组:

    ECStack = [];
    

    现在 javascript遇到下面这段代码了

    let a = 'hello world';
    
    function first () {
    	console.log('进入 first 函数执行上下文');
    	second();
    	console.log('再次进入 first 函数执行上下文');
    }
    
    function second () {
    	console.log('进入 second 函数执行上下文');
    }
    
    first();
    
    console.log('进入 全局执行上下文(Global Execution Context)')
    

    当上述代码在浏览器加载时,Javascipt引擎创建了一个全局执行上下文并把它压入了执行上下文栈,用 globalContext表示它,并且只有当整个应用程序结束的时候(浏览器关闭),ECStack才会被清空,所以程序结束之前,ECStack最底部永远有个 globalContext:

    ECStack = [
        globalContext
    ];
    

    当执行到一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,就可以分析出 ECStack的变化过程:

    // 伪代码
    
    // first()
    ECStack.push(<first> functionContext);
    
    // first中调用了second,继续创建second的执行上下文
    ECStack.push(<second> functionContext);
    
    // second执行完毕
    ECStack.pop();
    
    // first执行完毕
    ECStack.pop();
    
    // javascript接着执行下面的代码,但是ECStack底层永远有个globalContext;
    

    image

    注意:函数中,遇到return能终止可执行代码的执行,因此会直接将当前上下文弹出栈。

    例如,看以下这个闭包例子:

    function f1(){
        var n=999;
        function f2(){
            alert(n);
        }
        return f2;
    }
    var result=f1();
    result(); // 999
    

    因为f1中的函数f2在f1的可执行代码中,并没有被调用执行,因此执行f1时,f2不会创建新的上下文,而直到result执行时,才创建了一个新的。具体演变过程如下:

    // 伪代码:
    // 全局上下文入栈:
    ECStack = [
        globalContext
    ];
    
    // f1 EC入栈:
    ECStack.push(<f1> functionContext);
    // f1 EC出栈:
    ECStack.pop();
    // result EC入栈:
    ECStack.push(<result> functionContext);
    // result EC出栈:
    ECStack.pop();
    

    3.执行上下文的生命周期

    3.1 创建阶段

    • 生成变量对象(Variable object, VO)
    • 建立作用域链(Scope chain)
    • 确定this指向

    3.2 执行阶段

    • 变量赋值
    • 函数引用
    • 执行其他代码

    在接下来的文章中将梳理创建阶段的这三个步骤。

  • 相关阅读:
    vb dll com 组件发布web servcies
    修改表字段
    实体类集合安某个字段排序
    jquery 页面追加换行等等操作备份
    表锁死 杀死线程
    jeecg 自定义loading框(导入时加载备份)
    UBoot200903移植笔记(第二阶段:时钟!)
    UBoot200903移植笔记(点亮第一展灯)
    UBoot200903移植笔记(从Nandflash启动一)
    UBoot200903移植笔记(从Nandflash启动二)
  • 原文地址:https://www.cnblogs.com/hezhi/p/10014996.html
Copyright © 2020-2023  润新知