Js 执行机制之调用栈

1,014 阅读4分钟

What?

用来管理函数调用关系的一种数据结构


调用栈是怎么产生的?

函数的调用过程

  • Javascript 引擎在预编译阶段,为全局代码创建全局执行上下文,全局执行上下文包括:
  • 变量环境:
    • 提升的变量
    • 提升后的函数定义
  • 词法环境:
  • 然后 Javascript 引擎在全局代码的解释执行阶段,执行到函数调用语句时,会自动判断这是函数调用:
    • 从全局执行上下文中取出函数内的代码
    • 对这段代码重复上面的编译阶段,并创建该函数的执行上下文和可执行代码
    • 然后执行这段可执行代码

调用栈因函数调用应运而生

通过分析上述函数的调用过程,可以得出 Javascript 在执行的过程中可能会有多个执行上下文,那么就需要一种 “工具” 去管理这个执行上下文了


什么是栈?

栈是一种只能在一端进行插入和删除操作的特殊线性表数据结构。
  • 按先进后出的原则存储数据,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
  • 栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针

什么是 Javascript 调用栈?

JavaScript 引擎利用栈结构来管理执行上下文,将执行上下文按照执行顺序压入到的这个栈就够就是 Js 的调用栈(也叫执行上下文栈),是 JavaScript 引擎追踪函数执行的一个机制

利用浏览器查看调用栈信息

/* 
    1、利用浏览器查看调用栈信息 
    2、用 console.trace() 打印函数调用关系信息
*/
var a = 2;
function foo(b,c){
    console.trace();
    debugger;
    return b + c;
}
function foo2(b,c){
    var d = 10;
    result = foo(b,c);
    return  a + result + d;
}
foo2(3,6);
  • 打开“开发者工具”,点击“Source”标签,选择 JavaScript 代码的页面,栈的最底部是 anonymous 全局函数入口


  • 使用 console.trace() 在控制台输出当前函数的调用关系



什么是栈溢出?

调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎报错的现象就是栈溢出,调用栈最大栈容量和最大调用深度,满足其中任意一个就会栈溢出

怎么优化栈溢出问题?

  • 把递归调用的形式改造成其他形式
    • 使用循环代替递归
    • 某些环境下使用尾递归,其宿主环境已经做了栈溢出的优化,目前只知道 Safari 版本 12.1.2 (14607.3.9) 支持尾递归优化
      • 尾递归是什么?
                    -- 一个函数中所有递归形式的调用都出现在函数的末尾
      • 尾递归的特点?
                    -- 在回归过程中不用做任何操作
      • 尾递归的优化原理?
                    -- 当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要

  • 当前任务拆分为其他很多小任务

/* 
    优化栈溢出代码
    代码分析:
        1、该段代码导致栈溢出的原因是什么?
        -- 是 foo3 函数使用了递归函数调用自身,当 n 的值超过浏览器最大调用栈时超出的
        2、可以根据产生问题的原因可以从下面方案优化
            (1)、使用其他方式实现该递归函数,即任务执行过程中不再创建新的执行上下文,只在当前函数执行山下文中重复执行任务
            (2)、使用定时器将调用栈的任务拆分为多个定时器的任务
*/

/* 使用 while 循环实现递归函数,取消代码块进调用栈 */
function foo4 (n) {
    while(true){
        if(n <= 0){
            return 'stop';
        }
        n--;
    }
}
// console.log(foo4(12576));

/* 使用定时器将当前任务调用 */
function foo5 (n) {
    if (n === 0) {
        return 'stop';
    };
    console.trace();
    return setTimeout(()=>{
        foo5( n - 1);
    });
}
console.log(foo5(12576));