浏览器工作原理与实践
李兵
前盛大创新院高级研究员
立即订阅
6167 人已学习
课程目录
已完结 42 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 参透了浏览器的工作原理,你就能解决80%的前端难题
免费
宏观视角下的浏览器 (6讲)
01 | Chrome架构:仅仅打开了1个页面,为什么有4个进程?
02 | TCP协议:如何保证页面文件能被完整送达浏览器?
03 | HTTP请求流程:为什么很多站点第二次打开速度会很快?
04 | 导航流程:从输入URL到页面展示,这中间发生了什么?
05 | 渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的?
06 | 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?
浏览器中的JavaScript执行机制 (5讲)
07 | 变量提升:JavaScript代码是按顺序执行的吗?
08 | 调用栈:为什么JavaScript代码会出现栈溢出?
09 | 块级作用域:var缺陷以及为什么要引入let和const?
10 | 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
11 | this:从JavaScript执行上下文的视角讲清楚this
V8工作原理 (3讲)
12 | 栈空间和堆空间:数据是如何存储的?
13 | 垃圾回收:垃圾数据是如何自动回收的?
14 | 编译器和解释器:V8是如何执行一段JavaScript代码的?
浏览器中的页面循环系统 (6讲)
15 | 消息队列和事件循环:页面是怎么“活”起来的?
16 | WebAPI:setTimeout是如何实现的?
17 | WebAPI:XMLHttpRequest是怎么实现的?
18 | 宏任务和微任务:不是所有任务都是一个待遇
19 | Promise:使用Promise,告别回调函数
20 | async/await:使用同步的方式去写异步代码
浏览器中的页面 (8讲)
21 | Chrome开发者工具:利用网络面板做性能分析
22 | DOM树:JavaScript是如何影响DOM树构建的?
23 | 渲染流水线:CSS如何影响首次加载时的白屏时间?
24 | 分层和合成机制:为什么CSS动画比JavaScript高效?
25 | 页面性能:如何系统地优化页面?
26 | 虚拟DOM:虚拟DOM和实际的DOM有何不同?
27 | 渐进式网页应用(PWA):它究竟解决了Web应用的哪些问题?
28 | WebComponent:像搭积木一样构建Web应用
浏览器中的网络 (3讲)
29 | HTTP/1:HTTP性能优化
30|HTTP/2:如何提升网络速度?
31|HTTP/3:甩掉TCP、TLS 的包袱,构建高效网络
浏览器安全 (5讲)
32 | 同源策略:为什么XMLHttpRequest不能跨域请求资源?
33 | 跨站脚本攻击(XSS):为什么Cookie中有HttpOnly属性?
34 | CSRF攻击:陌生链接不要随便点
35 | 安全沙箱:页面和系统之间的隔离墙
36 | HTTPS:让数据传输更安全
结束语 (1讲)
结束语 | 大道至简
课外加餐 (4讲)
加餐一|浏览上下文组:如何计算Chrome中渲染进程的个数?
加餐二|任务调度:有了setTimeOut,为什么还要使用rAF?
加餐三|加载阶段性能:使用Audits来优化Web性能
加餐四|页面性能工具:如何使用Performance?
浏览器工作原理与实践
登录|注册

08 | 调用栈:为什么JavaScript代码会出现栈溢出?

李兵 2019-08-22
上篇文章中,我们讲到了,当一段代码被执行时,JavaScript 引擎先会对其进行编译,并创建执行上下文。但是并没有明确说明到底什么样的代码才算符合规范。
那么接下来我们就来明确下,哪些情况下代码才算是“一段”代码,才会在执行之前就进行编译并创建执行上下文。一般说来,有这么三种情况:
当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
好了,又进一步理解了执行上下文,那本节我们就在这基础之上继续深入,一起聊聊调用栈。学习调用栈至少有以下三点好处:
可以帮助你了解 JavaScript 引擎背后的工作原理;
让你有调试 JavaScript 代码的能力;
帮助你搞定面试,因为面试过程中,调用栈也是出境率非常高的题目。
比如你在写 JavaScript 代码的时候,有时候可能会遇到栈溢出的错误,如下图所示:
栈溢出的错误
那为什么会出现这种错误呢?这就涉及到了调用栈的内容。你应该知道 JavaScript 中有很多函数,经常会出现在一个函数中调用另外一个函数的情况,调用栈就是用来管理函数调用关系的一种数据结构。因此要讲清楚调用栈,你还要先弄明白函数调用栈结构
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《浏览器工作原理与实践》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(59)

  • 徐承银
    不进栈,就不会栈溢出了。function runStack (n) {
      if (n === 0) return 100;
      return setTimeout(function(){runStack( n- 2)},0);
    }
    runStack(50000)
    2019-08-22
    9
  • ytd
    改成循环不会栈溢出了,不过就有可能陷入死循环:
    // 优化
    function runStack(n) {
        while (true) {
            if (n === 0) {
                return 100;
            }

            if (n === 1) { // 防止陷入死循环
                return 200;
            }

            n = n - 2;
        }
    }

    console.log(runStack(50000));
    2019-08-22
    7
  • mfist
    1. 改成尾递归调用(需要在严格模式下面生效)
    function runStack (n, result=100) {
      if (n === 0) return result;
      return runStack( n- 2, result);
    }
    runStack(50000, 100)
    2. 改成循环调用,不使用递归函数,就不存在堆栈溢出

    作者回复: 有尝试过尾优化会生效吗?

    2019-08-22
    5
    5
  • 一步
    关于调用栈的大小,不用的平台,比如浏览器,nodejs 怎么查看设置的,还是硬编码的?

    作者回复: 调用栈有两个指标,最大栈容量和最大调用深度,满足其中任意一个就会栈溢出,不过具体多大和多深,这个没有研究过,你可以拿我留的作业那段代码去各平台测试下,应该很快就能测试出来最大调用深度。

    2019-08-24
    4
  • pyhhou
    思考题当中的函数,如果输入参数是正偶数,那么不管数值多大,最后结果都是 100,除此之外,如果输入参数是负数或者是正奇数,甚至说是浮点数,那么使用递归方式调用会导致栈溢出,使用循环方式去实现会导致死循环,如果仅仅是基于当前的输入参数(50000)改写的话:
    function runStack (n) {
      return 100;
    }
    runStack(50000);

    当然也可以把递归改成循环的写法,但是要注意的是这时的输入参数仅限定于正偶数,否则会死循环:
    function runStack (n) {
      while (n !== 0) {
        n -= 2;
      }

      return 100;
    }
    runStack(50000);

    作者回复: 可能我的题目出得不太好,误解我出题目的意思了,我的意思是runStack要执行50000次的,但是要避免栈溢出,改成斐波那契数列的列子可能好点。

    2019-08-23
    4
  • Geek_8476da
    我测试出了栈的深度为12574
    2019-09-10
    2
    3
  • Ryan
    https://kangax.github.io/compat-table/es6/ 这个网站可以看到各平台对作为es6特性的尾调用优化的支持情况,表格里面显示:桌面浏览器中只有safari 12 支持尾调用优化。我自己使用safari 12测试,严格模式下运行作者代码能正常得出结果。https://node.green/#ESNEXT-strawman--stage-0--syntactic-tail-calls 这个网站则显示,目前版本的node.js不支持尾递归优化
    2019-09-03
    3
  • melon
    老师没有提函数的入参和返回值,函数的入参和返回值是不是也在函数上下文的变量环境里呢?
    2019-10-17
    2
  • 黄晓杰
    老师,我有一个疑问,调用栈是后进先出,那么当存在闭包时,某个函数的执行上下文还存在,那么其他函数的出栈是否受影响?

    作者回复: 执行上下文已经没了,只不过内部函数引用的变量还保存在堆上,所以不影响栈的操作,后面一节有分析这个问题!

    2019-09-11
    2
  • AICC
    老师会在什么地方讲解每节内容留下的思考题?比如像这节的尾调用问题,是否存在尾调用优化,至少目前看到的尝试方式在chrome上都会出现栈溢出,有说是v8移除了TCO,即尾调用优化,参考:https://stackoverflow.com/questions/42788139/es6-tail-recursion-optimisation-stack-overflow
    还有目前google提供的优化方式,但在chrome上目前还不支持,如下
    function factorial(n, acc = 1) {
      if (n === 1) return acc;
      return continue factorial(n - 1, acc * n)
    }
    2019-08-27
    2
  • 许童童
    将递归改成迭代就好了,还可以使用尾递归优化。感觉老师这道题改成斐波那契数列会更好。

    function runStack (n) {
      while (n > 0) {
        n -= 2
      }
      return 100
    }
    runStack(50000)

    作者回复: 是的 改成斐波那契数列会好点。

    不过尾部优化似乎是没效果的

    2019-08-22
    3
    2
  • 曾侃
    老师好,第三张图函数调用过程是不是有点问题,函数的可执行代码里面是不是应该包含 b = 10?
    2019-09-30
    1
  • 轩爷
    亲测,Chrome【版本 77.0.3865.75(正式版本)(64 位)】和Firefox【67.0.4 (64 位)】都不支持尾调用优化,只有Safari【版本 12.1.2 (14607.3.9)】支持
    2019-09-15
    1
  • yetta_xy
    老师您好,有个疑问。
    addAll函数中的result变量没有用var声明,直接赋值的,这个变量应该存在于全局上下文的环境变量对象中吧?
    2019-09-07
    1
    1
  • 郭纯
    解决这样的问题 有几种方法 比如 不用递归 用循环 采用延时执行.最好的方法采用生成器 generator 参考协同的实现.
    2019-09-04
    1
  • 安思科
    就您说的只要把递归的那个return用setTimeout包一下就行了吧
    2019-08-23
    1
  • 忘忧草的约定
    我理解将内部的函数当成一个独立的子任务放到microstack任务队列里面去就可以了吧,这样当该任务执行的时候调用栈里面只有全局上下文
    2019-08-22
    1
  • 爱吃锅巴的沐泡
    老师,有两个问题:
    1、老师在文中写到“首先,从全局执行上下文中,取出 add 函数代码。”,这里是取到函数的引用,还是整个函数代码,函数的存储是怎样的?
    2、声明带参的函数并调用的编译过程是怎样的,参数应该是和arguments有关吧,老师能详细说一下编译过程嘛?

    作者回复: 第一个:看成是一个引用,函数实体是保存到堆中的。堆栈结构后面章节会介绍。

    第二个:如果一个函数带有参数,编译过程中,参数会通过参数列表保存到变量环境中

    2019-08-22
    1
  • Marvin
    这个代码运行情况依赖入参,有三种情况:1、n=0,返回100;2、n为正偶数,递归n/2次之后返回100;3、n为非上述情况,栈溢出。优化方案:判断参数n,1、2两种情况返回100,3的情况抛错。
    2019-08-22
    1
    1
  • Hurry
    将递归,改成循环:
    ```
    function runStack(n) {
            if (n === 0)
                return 100;
        }

        function run(n) {
            while (n > 0) {
                runStack(n)
                n = n - 2;
            }

            return runStack(n)
        }

        run(50000)
    ```
    2019-08-22
    1
收起评论
59
返回
顶部