JavaScript 代码的执行流程
一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。执行阶段则是指解释器解释执行字节码,或者是CPU直接执行二进制机器代码的阶段。
编译阶段
这个阶段,会将通过var声明的变量和函数声明进行提升。并且生成两个部分:执行上下文和可执行代码。
执行上下文是 JavaScript 执行一段代码时的运行环境。并且变量提升的内容就保存在这个执行上下文的变量环境的对象中。
执行阶段
就是分析可执行代码,并对变量环境中提升的变量做赋值操作。执行阶段是从上向下执行的,遇到相同的变量名和函数,会进行相互覆盖的。
function showName() {
console.log('极客邦');
}
showName();
// function showName() {
// console.log('极客时间');
// }
var showName = "zh";
showName(); // showName is not a function
请分析一下这段代码执行结果?
showName()
function showName() {
console.log(1)
}
var showName = function() {
console.log(2)
}
- 首先,showName会被提升并赋值为undefined。
- 然后函数声明也会进行提升,并且会覆盖变量的声明提升。
- 最后到了执行阶段,执行showName,将输出1。
调用栈
调用栈就是用来管理函数调用关系的一种数据结构。、
在执行js代码时,会创建很多的执行上下文,每创建一个执行上下文,他将被压入调用栈中。
当我们在编译阶段,就创建了全局执行上下文,并将其压入栈底。但是只有当函数调用的时候,才会创建函数执行上下文,并将其压入到栈中。
可以通过浏览器开发者工具中通过打断点来查看调用栈(call stack)。也可以通过console.trace()
打印出当前调用栈信息。
栈溢出
调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。
如何改变下面的代码,让其不会出现栈溢出?
function runStack (n) {
if (n === 0) return 100;
return runStack( n- 2);
}
runStack(50000)
通过定时器。
function runStack (n) {
if (n === 0) return 100;
return setTimeout(function(){runStack( n- 2)},0);
}
runStack(50000)
蹦床函数
function runStack (n) {
if (n === 0) return 100;
return setTimeout(function(){runStack( n- 2)},0);
}
console.log(runStack(50000))
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;
}
console.log(trampoline(runStack(1000000)))
通过while循环
function runStack(n) {
while(true) {
if(n == 1 || n == 0) {
return 100;
}
n -= 2;
}
}
console.log(runStack(50000))
计算机是如何执行二进制代码的
我们知道,高级语言都需要被编译成字节码或者二进制代码才能被二进制代码执行。
程序的执行,本质上就是 CPU 按照顺序执行这一大堆指令的过程。
我们先来了解一下计算机的硬件构成。 首先,在程序执行之前,我们的程序需要被装进内存。内存是一个临时存储数据的设备,之所以是临时的存储器,是因为断电之后,内存中的数据都会消失。
CPU 可以通过指定内存地址,从内存中读取数据,或者往内存中写入数据,有了内存地