浏览器原理 07 # 调用栈:为什么JavaScript代码会出现栈溢出?

简介: 浏览器原理 07 # 调用栈:为什么JavaScript代码会出现栈溢出?

说明

浏览器工作原理与实践专栏学习笔记



三种情况


什么样的代码才会在执行之前就进行编译并创建执行上下文?


当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。


当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。


当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。



调用栈

栈溢出的错误:


20210318150657105.png

出现这种错误就涉及到了调用栈的内容。调用栈就是用来管理函数调用关系的一种数据结构。因此要讲清楚调用栈,要先弄明白函数调用栈结构


什么是函数调用

函数调用就是运行一个函数,具体使用方式是使用函数名称跟着一对小括号。


例子:

var a = 2
function add(){
  var b = 10
  return  a+b
}
add()



上面例子的全局执行上下文:

20210318151259486.png



  1. 从全局执行上下文中,取出 add 函数代码。
  2. 对 add 函数的这段代码进行编译,并创建该函数的执行上下文可执行代码
  3. 执行代码,输出结果。



完整流程


20210319170949426.png


当执行到 add 函数的时候,就有了两个执行上下文

  1. 全局执行上下文
  2. add 函数的执行上下文

而这些执行上下文是通过一种叫栈的数据结构来管理的。



什么是栈

栈就是类似于一端被堵住的单行线,车子类似于栈中的元素,栈中的元素满足后进先出的特点。

栈容器、入栈、栈满、出栈、空栈


20210319182426201.png




什么是 JavaScript 的调用栈

通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈

示例:

var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
  var d = 10
  result = add(b,c)
  return  a+result+d
}
addAll(3,6)


第一步,创建全局上下文,并将其压入栈底。

全局执行上下文压栈

2021032010170075.png


赋值操作改变执行上下文中的值


20210320101825697.png



第二步是调用 addAll 函数。

执行 addAll 函数时的调用栈


20210320101914252.png



第三步,当执行到 add 函数

执行 add 函数时的调用栈

20210320102118824.png



当 add 函数返回时,该函数的执行上下文就会从栈顶弹出

20210320102357758.png



addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文。


20210320102446379.png


调用栈是 JavaScript 引擎追踪函数执行的一个机制。



如何利用好调用栈


1. 如何利用浏览器查看调用栈的信息


  1. 打开“开发者工具”
  2. 点击“Source”标签
  3. 选择 JavaScript 代码的页面,然后在第 3 行加上断点,并刷新页面。
  4. 你可以看到执行到 add 函数时,执行流程就暂停了
  5. 通过右边“call stack”来查看当前的调用栈的情况
  6. 栈的最底部是 anonymous,也就是全局的函数入口


20210320103556692.png


除了通过断点来查看调用栈,还可以使用 console.trace() 来输出当前的函数调用关系


20210320175449399.png



2. 栈溢出(Stack Overflow)


调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出


写递归代码的时候,就很容易出现栈溢出的情况:超过了最大栈调用大小(Maximum call stack size exceeded)。

function division(a,b){
    return division(a,b)
}
console.log(division(1,2))


20210320175642892.png



原因如下:

  1. 首先调用函数 division,并创建执行上下文,压入栈中
  2. 这个函数是递归的,并且没有任何终止条件,它会一直创建新的函数执行上下文,并反复将其压入栈中
  3. 栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。




同时这个引发栈溢出的栈大小由于浏览器的厂商、版本各不相同。

那么怎么知道不同版本的限制大小?

可以在不同的浏览器控制台输入下面的代码

var i = 0;
function inc() {
  i++;
  inc();
}
try {
  inc();
}
catch(e) {
  // The StackOverflow sandbox adds one frame that is not being counted by this code
  // Incrementing once manually
  i++;
  console.log('Maximum stack size is', i, 'in your current browser');
}



   Internet Explorer

       IE6: 1130

       IE7: 2553

       IE8: 1475

       IE9: 20678

       IE10: 20677


   Mozilla Firefox

       3.6: 3000

       4.0: 9015

       5.0: 9015

       6.0: 9015

       7.0: 65533

       8b3: 63485

       17: 50762

       18: 52596

       19: 52458

       42: 281810


   Google Chrome

       14: 26177

       15: 26168

       16: 26166

       25: 25090

       47: 20878

       51: 41753


   Safari

       4: 52426

       5: 65534

       9: 63444


   Opera

       10.10: 9999

       10.62: 32631

       11: 32631

       12: 32631


   Edge

       87: 13970


参考资料:Browser Javascript Stack size limit



总结


   每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。


   如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。


   当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。


   当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。



思考题


问题


你能优化下这段代码,以解决栈溢出的问题吗?


执行下面代码:

function runStack (n) {
  if (n === 0) return 100;
  return runStack( n - 2);
}
runStack(50000)


20210320182629784.png



参考


使用es6的蹦床函数解决递归造成的堆栈溢出

蹦床函数:结合.bind,使函数调用的时候是自己的方法,但是确是另一个函数对象,不是本身,这个时候就不会造成内存的泄露,发生堆栈溢出了,实现代码如下:

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}


代码实现

function runStack (n) {
  if (n === 0) return 100;
  return runStack.bind(null, n - 2); // 返回自身的一个版本
}
// 蹦床函数,避免递归
function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}
trampoline(runStack(1000000))



20210320184520439.png




目录
相关文章
|
3天前
|
JavaScript
浏览器插件crx文件--JS混淆与解密
浏览器插件crx文件--JS混淆与解密
9 0
|
16天前
|
JSON JavaScript 前端开发
JavaScript原生代码处理JSON的一些高频次方法合集
JavaScript原生代码处理JSON的一些高频次方法合集
|
23天前
|
JavaScript 前端开发 UED
JS:如何获取浏览器窗口尺寸?
JS:如何获取浏览器窗口尺寸?
33 1
|
3天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。
|
7天前
|
JavaScript 前端开发
JavaScript如何获得浏览器的宽高
JavaScript如何获得浏览器的宽高
|
10天前
|
JavaScript 前端开发 安全
JavaScript DOM 操作:解释一下浏览器的同源策略。
**同源策略**是浏览器安全基石,它阻止脚本跨不同协议、域名或端口访问资源,防止恶意行为。例如,HTTP页面无法直接用JS获取HTTPS页面内容。**CORS**允许跨域请求,但需服务器配合设置,通过`document.domain`属性可配置,但仍受限于服务器配置。
14 4
|
11天前
|
监控 前端开发 JavaScript
如何使用浏览器调试前端代码?
【4月更文挑战第11天】前端开发中,浏览器调试是关键技能,能提升代码质量。本文介绍了如何使用浏览器的调试工具:1) 打开调试窗口(F12或右键检查);2) Elements标签页检查DOM结构和样式;3) Console调试JavaScript,查看日志和错误信息;4) Sources设置断点调试JS文件;5) 利用Network、Performance和Memory等标签页优化性能。熟悉调试工具、利用日志和错误信息能有效定位问题,提高开发效率。
28 7
|
16天前
|
JavaScript
【归总】原生js操作浏览器hash、url参数参数获取/修改方法合集
【归总】原生js操作浏览器hash、url参数参数获取/修改方法合集
|
17天前
|
搜索推荐 前端开发 UED
html页面实现自动适应手机浏览器(一行代码搞定)
html页面实现自动适应手机浏览器(一行代码搞定)
18 0
|
21天前
|
存储 JavaScript 前端开发
在浏览器中存储数组和对象(js的问题)
在浏览器中存储数组和对象(js的问题)

热门文章

最新文章