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

说明

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

三种情况

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

  1. 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  3. 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。

调用栈

栈溢出的错误:

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

什么是函数调用

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

例子:

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

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

在这里插入图片描述

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

完整流程

在这里插入图片描述
当执行到 add 函数的时候,就有了两个执行上下文

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

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

什么是栈

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

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

在这里插入图片描述

什么是 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)

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

全局执行上下文压栈

在这里插入图片描述

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

在这里插入图片描述

第二步是调用 addAll 函数。

执行 addAll 函数时的调用栈

在这里插入图片描述

第三步,当执行到 add 函数

执行 add 函数时的调用栈

在这里插入图片描述

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

在这里插入图片描述
addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文。

在这里插入图片描述

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

如何利用好调用栈

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

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

在这里插入图片描述
除了通过断点来查看调用栈,还可以使用 console.trace() 来输出当前的函数调用关系

在这里插入图片描述

2. 栈溢出(Stack Overflow)

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

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

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

在这里插入图片描述

原因如下:

  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

总结

  1. 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
  2. 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
  3. 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。
  4. 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。

思考题

问题

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

执行下面代码:

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

在这里插入图片描述

参考

使用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))

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凯小默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值