作为前端开发者,你是否也曾对着一段看似简单的 JS 代码,发出 "它为啥这么执行?" 的灵魂拷问?比如明明变量还没赋值却不报错,明明声明了变量却提示未定义...... 其实这一切的背后,都是 JS 执行机制在 "搞事情"。今天咱们就扒开 V8 引擎的 "小脑袋",把 JS 执行的底层逻辑聊得明明白白!
一、执行上下文:JS 代码的 "运行大本营"
想要搞懂 JS 执行,先得认识「执行上下文」------ 它就像代码运行的 "专属大本营",每段 JS 代码(全局 / 函数)执行前,都会先创建这个对象,里面装着代码运行所需的所有 "物料"。
执行上下文里核心包含三个部分:
- 变量环境 :专门收纳
function和var声明的变量 / 函数,是老派变量的 "专属座位区"; - 词法环境 :
let/const的 "新地盘",最大特点是支持块级作用域; - 执行的代码:按从上到下的顺序执行的代码逻辑。
而管理这些 "大本营" 的,就是「执行栈(调用栈)」------V8 引擎用它管理函数调用关系的栈结构,栈顶永远是当前正在执行的上下文,函数执行完就 "弹出" 销毁,主打一个 "来了就占座,走了就清场"。
二、编译先行:JS 执行的 "预习环节"
JS 并非 "边读边执行",而是遵循「先编译,后执行」的原则,编译发生在代码执行前的一瞬间。编译阶段会完成这四件事(函数上下文专属,全局上下文无步骤 3):
- 创建执行上下文对象;
- 找形参和变量声明,变量名作为 key,值先设为
undefined(这就是 var "变量提升" 的根源); - 形参和实参值统一(比如函数传参时,形参会被实参赋值);
- 找函数声明,函数名作为 key,值直接赋值为函数体(函数提升优先级比变量高)。
实例 1:变量提升 vs 函数提升(来自 2.js/3.js)
javascript
运行
javascript
// 案例1:var变量提升 + 函数提升
showName(); // 输出:函数执行(函数提升,优先执行)
console.log(myname); // 输出:undefined(var变量提升,值为undefined)
var myname = '邓昌兴'; // 执行阶段赋值
function showName() {
console.log('函数执行');
}
// 编译阶段发生了啥?
// 1. 创建全局执行上下文;
// 2. 找到var myname,存入变量环境:{ myname: undefined };
// 3. 找到function showName,存入变量环境:{ showName: 函数体 };
// 4. 执行阶段:先调用showName(已有函数体),再打印myname(还是undefined),最后赋值。
// 案例2:函数提升优先级高于变量提升(来自3.js)
console.log(func); // 输出:[Function: func](函数提升覆盖变量提升)
function func() {}
var func = '123';
// 编译阶段:先处理var func(设为undefined),再处理function func(赋值为函数体);
// 执行阶段:打印func时,取到的是函数体,而非undefined。
实例 2:函数参数 + 变量提升的 "连环套"(来自 4.js)
javascript
运行
css
var a = 1;
function fn(a) {
// 编译阶段:
// 1. 形参a入变量环境:{ a: undefined };
// 2. 实参赋值:a = 3;
// 3. 找到function a(),覆盖a:{ a: 函数体 };
// 4. 找到var a = 2(已声明,无操作)、var b = undefined;
console.log(a); // 输出:[Function: a](编译后a是函数体)
var a = 2; // 执行阶段:a赋值为2
function a() {}
var b = a; // 执行阶段:b赋值为2
console.log(a); // 输出:2
}
fn(3);
// 解析:函数编译时,函数声明优先级 > 形参 > 变量声明,所以第一步打印的是函数体,而非实参3。
三、let/const:变量界的 "新规矩"
let/const作为 ES6 的新特性,彻底打破了var的 "随性",核心特点全藏在词法环境里:
1. 块级作用域:变量的 "区域锁"(来自 5.js/6.js)
javascript
运行
javascript
// 案例1:var无块级作用域(来自5.js)
function varTest1() {
var a = 1;
if (true) {
var a = 2; // 同属函数作用域,覆盖外层a
console.log(a); // 输出:2
}
console.log(a); // 输出:2(if块里的a覆盖了外层)
}
varTest1();
// 案例2:let有块级作用域(来自6.js)
function varTest2() {
var a = 1;
if (true) {
let a = 2; // 块级作用域,和外层a互不干扰
console.log(a); // 输出:2(块内的a)
}
console.log(a); // 输出:1(外层的a)
}
varTest2();
// 案例3:块级作用域的精细化管理(来自7.js)
function foo() {
var a = 1; // 变量环境:a=1
let b = 2; // 词法环境:b=2
{
// 块级词法环境,独立于外层
let b = 3; // 块内b,和外层b无关
var c = 4; // var无块级作用域,存入函数的变量环境
let d = 5; // 块内词法环境:d=5
console.log(a); // 输出:1(取外层变量环境的a)
console.log(b); // 输出:3(取块内词法环境的b)
}
console.log(b); // 输出:2(取外层词法环境的b)
console.log(c); // 输出:4(var无块级作用域,能访问)
// console.log(d); // 报错:d is not defined(块级作用域的d,外层访问不到)
}
foo();
2. 禁止重复声明:变量的 "唯一性校验"(来自 8.js)
javascript
运行
vbnet
let a = 1;
// let a = 2; // 报错:Identifier 'a' has already been declared
function a() {} // 同样报错:let声明的a不能重复定义
// 解析:let/const不允许同一作用域内重复声明变量,哪怕是函数声明也不行;而var允许重复声明(后声明覆盖前声明)。
3. 暂时性死区(TDZ):变量的 "未解锁期"
let/const没有变量提升,在声明前访问会直接报错,这个区间就是「暂时性死区」:
javascript
运行
javascript
console.log(b); // 报错:Cannot access 'b' before initialization
let b = 2;
// 解析:编译阶段,let变量会被存入词法环境,但不会像var一样赋值为undefined;
// 执行阶段,声明前访问就会触发死区报错。
四、JS 执行流程:一张图总结
plaintext
markdown
读取代码 → 编译(创建执行上下文:变量环境+词法环境)→ 执行(按顺序跑代码)
↓ ↓
处理变量/函数声明 调用栈管理执行上下文(栈顶=当前执行的上下文)
最后:核心总结
- JS 执行的核心是「执行上下文」,编译永远发生在执行前;
var/function进变量环境,有提升特性,无块级作用域;let/const进词法环境,支持块级作用域,无提升、有死区、禁止重复声明;- 调用栈是执行上下文的 "管家",栈顶永远是当前正在执行的代码,执行完就销毁。
搞懂这些,再遇到 "代码为啥这么跑" 的问题,你就能直接 "透视" V8 引擎的执行逻辑,再也不用靠 "猜" 写代码啦!