javaScript是一门非常强大的语言,它既能做前端也能做后端;在JavaScript中,代码在执行之前有一个非常重要的步骤,那就是预编译,它会帮助确定变量和函数的作用域,并为它们分配内存空间。预编译发生在代码执行前,这种工作方式就犹如女秘书一般,在老板工作之前,把任务一一安排好;方便后续工作有条不紊的进行。今天就让我们一起撕破“女秘书”神秘的面纱。
预编译
声明提升
编译就是整理代码中的规则,让 v8(javaScript的执行引擎) 能够读懂我们写的代码;首先来看下面一段代码:
console.log(a);
var a = 123
正常来说,你看到代码的第一眼是会觉得它是会报错的;因为变量的声明在执行操作的下面,所以它读取不到a的值;但是这段代码在v8执行之后会输出 undefined ,v8之所以能够输出这个值,就是取决于js中的预编译。
预编译主要会干以下三个步骤:
- 词法分析
- 解析
- 生成代码
所以在v8眼里,执行完预编译之后代码应该是这样的;
var a; // 声明一个变量a,不赋值,所以为undefined
console.log(a);
a = 123; // 进行赋值操作
从上面可以看到,预编译会把var声明的变量给提升到前面;对于变量,只有使用var声明的变量会被提升,而let和const声明的变量不会被提升,它们具有块级作用域。所以要是换成let声明,以上代码就会报错。
同样的,函数声明(function declaration)也会被提升。
foo()
// 函数声明提升
function foo() {
var a = 1;
console.log(a);
}
需要注意的是,在声明提升当中;变量声明时,只有它的声明会被提升前面,对这个变量的赋值操作并不会一起提升;而在函数声明中,整个函数体都会被提升到前面。
函数中的预编译
函数中的预编译主要会执行以下的一些步骤:
- 创建函数的执行上下文对象 AO {Activation Object}
- 找形参和变量声明,将形参和变量名作为AO的属性,值为undefined
- 将实参和形参统一
- 在函数体内找函数声明,将函数名作为AO的属性名,值赋予函数体
function fn(a) {
console.log(a);
var a = 123
console.log(a);
function a() {}
console.log(a);
var b = function () {}
console.log(b);
function d() {}
var d = a
console.log(d);
}
fn(1);
以上面的代码为例,我们可以模拟一下函数中预编译的过程:
// 创建函数的执行上下文对象,AO
AO:{
a: undefined -> 1 -> function () {} -> 123,
b: undefined -> function () {},
d: undefined -> function () {},
}
在上面的预编译过程中,首先“女秘书”会先创建函数的执行上下文对象,AO;然后,开始找到函数中的形参和变量声明,找到三个变量声明a、b、d,并把它们的值都赋为,undefined,再添加到AO对象中;接着,把函数的形参和实参统一,即由fn(1),可以把形参a变为1;最后,再找到函数中的函数声明,function a(),b(),d();由于在AO对象中不能出现两个相同的key,所以“女秘书”直接会把a,b,d的值修改为函数体function () {};这样预编译的工作就执行完了。
全局的预编译
“女秘书”在完成函数的预编译后,同样也要完成全局中的预编译过程;主要步骤如下:
- 创建全局执行上下文对象 GO(Global Object)
- 找变量声明,变量名作为GO的属性名,值为undefined
- 在全局找函数声明,函数名作为GO的属性名,值为函数体
与函数预编译过程比起来,少了统一形参实参的一步,因为在全局中没有形参;其他步骤基本都是一个道理。
global = 100
function fn() {
console.log(global);
global = 200
console.log(global);
var global = 300
}
fn()
var global;
我们再来模拟一下全局的预编译过程:
GO:{
global: undefined -> 100,
fn: function(){},
}
AO:{
global: undefined
}
首先创建全局的执行上下文对象,GO;再找变量声明,找到变量global,变量名作为GO的属性名,值赋为undefined;最后在全局找函数声明,找到函数fn,函数名fn作为GO的属性名,值赋为函数体function(){};这样就完成了全局的一个预编译过程。
调用栈
何为调用栈?调用栈(Call Stack)是编程中的一个重要概念,特别是在涉及函数调用和执行流的跟踪时。它是一个数据结构,通常以栈(Last In, First Out, LIFO)的形式存储函数调用的信息。每当一个函数被调用时,相关信息(如函数返回地址、局部变量、参数等)会被压入(push)调用栈的顶部;当函数执行结束并返回时,相关信息会被弹出(pop)栈,控制权返回到调用者。
var a = 2
function add() {
var b = 10
return a + b
}
add()
其实在预编译完成后,代码执行时,就会把预编译过程中创建的GO和AO对象给逐一压入调用栈;然后v8就会顺着调用栈的顺序从上往下查找需要的变量。
调用栈是用来管理函数调用关系的一种数据结构,每当执行完一个函数的调用,这个函数的执行上下文对象就会出栈然后被销毁。
小结
在js中,预编译和调用栈是理解JavaScript执行流程中的两个重要概念;预编译发生在函数即将执行前,是JavaScript引擎为函数执行做准备的一个阶段;预编译阶段主要处理函数内部的环境和变量声明,确保函数准备好执行;调用栈则负责管理函数调用的执行顺序,保证程序逻辑的有序执行和正确的控制流。
你明白了吗?