说明
浏览器工作原理与实践专栏学习笔记
三种情况
什么样的代码才会在执行之前就进行编译并创建执行上下文?
- 当 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))