说明
浏览器工作原理与实践专栏学习笔记
三种情况
什么样的代码才会在执行之前就进行编译并创建执行上下文?
当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
调用栈
栈溢出的错误:
出现这种错误就涉及到了调用栈的内容。调用栈就是用来管理函数调用关系的一种数据结构。因此要讲清楚调用栈,要先弄明白函数调用和栈结构。
什么是函数调用
函数调用就是运行一个函数,具体使用方式是使用函数名称跟着一对小括号。
例子:
var a = 2 function add(){ var b = 10 return a+b } add()
上面例子的全局执行上下文:
- 从全局执行上下文中,取出 add 函数代码。
- 对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。
- 执行代码,输出结果。
完整流程
当执行到 add 函数的时候,就有了两个执行上下文
- 全局执行上下文
- 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. 如何利用浏览器查看调用栈的信息
- 打开“开发者工具”
- 点击“Source”标签
- 选择 JavaScript 代码的页面,然后在第 3 行加上断点,并刷新页面。
- 你可以看到执行到 add 函数时,执行流程就暂停了
- 通过右边“call stack”来查看当前的调用栈的情况
- 栈的最底部是 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))
原因如下:
- 首先调用函数 division,并创建执行上下文,压入栈中
- 这个函数是递归的,并且没有任何终止条件,它会一直创建新的函数执行上下文,并反复将其压入栈中
- 栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。
同时这个引发栈溢出的栈大小由于浏览器的厂商、版本各不相同。
那么怎么知道不同版本的限制大小?
可以在不同的浏览器控制台输入下面的代码
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)
参考
蹦床函数:结合.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))