引言:从执行到内存,理解 JS 的底层逻辑
在上篇中,我们揭开了 JavaScript 编译与执行的面纱,理解了变量提升、执行上下文和调用栈的运作机制。然而,真正的挑战在于:当函数被调用、变量被赋值、对象被修改时,内存中究竟发生了什么?
为什么 str2 = str 修改后不影响原字符串,而 obj2 = obj 却会同步改变?为什么函数参数 a 在内部被重新赋值后,外部传入的值却不受影响?这些问题的答案,藏在 JavaScript 的内存模型与值传递机制之中。
本文将继续深入 V8 引擎的内部世界,从内存分配到参数绑定,从简单类型到复杂对象,全面解析 JavaScript 的底层执行逻辑。
一、内存模型:栈与堆的分工协作
JavaScript 的内存分为两大区域:
1.1 栈内存(Stack):存储简单数据
- 存放基本数据类型:
string、number、boolean、null、undefined、symbol、bigint - 特点:值直接存储在栈中,访问速度快,生命周期短
let str = 'hello'; // 'hello' 直接存于栈
let num = 42; // 42 直接存于栈
1.2 堆内存(Heap):存储复杂数据
- 存放引用类型:
object、array、function - 栈中只保存指向堆内存的引用地址
- 特点:数据体积大,生命周期长,由垃圾回收器管理
let obj = { name: '张三' };
// 栈中:obj → [引用地址]
// 堆中:{ name: '张三' }
这种设计既保证了简单数据的高效访问,又避免了复杂对象对栈空间的过度占用。
二、值拷贝 vs 引用拷贝:理解赋值的本质
2.1 简单类型的值拷贝
let str = 'hello';
let str2 = str; // 值拷贝:str2 获得 'hello' 的副本
str2 = '你好';
console.log(str, str2); // 'hello' '你好'
因为字符串是基本类型,str2 = str 实际是复制值,两者互不影响。
2.2 复杂类型的引用拷贝
let obj = { name: '张三' };
let obj2 = obj; // 引用拷贝:obj2 和 obj 指向同一个对象
obj2.name = '李四';
console.log(obj, obj2); // { name: '李四' } { name: '李四' }
这里 obj2 = obj 只是复制了引用地址,两者仍指向堆中的同一个对象。因此,修改任一变量都会影响另一个。
这就是为什么在 React/Vue 中,更新状态必须使用新对象,而非直接修改原对象——否则视图无法正确响应变化。
三、函数参数:一切都是值传递
JavaScript 中所有参数传递都是值传递,但“值”的含义因类型而异:
3.1 基本类型:传递实际值
function changeNum(x) {
x = 100;
}
let num = 50;
changeNum(num);
console.log(num); // 50(未改变)
函数内部的 x 是 num 的副本,修改 x 不影响 num。
3.2 引用类型:传递引用的副本
function changeObj(o) {
o.name = '王五'; // 修改属性 → 影响原对象
o = { name: '赵六' }; // 重新赋值 → 不影响原对象
}
let person = { name: '张三' };
changeObj(person);
console.log(person); // { name: '王五' }
o.name = '王五':通过引用修改堆中对象 → 影响原对象o = { ... }:让o指向新对象 → 不影响原引用
这解释了为何函数可以修改对象属性,却不能“替换”整个对象。
四、严格模式:让 JS 更安全、更规范
通过 'use strict' 启用严格模式,JavaScript 会施加更多限制:
4.1 禁止隐式全局变量
'use strict';
a = 1; // ReferenceError: a is not defined
4.2 禁止重复声明(即使是 var)
'use strict';
var a = 1;
var a = 2; // SyntaxError: Identifier 'a' has already been declared
注意:在非严格模式下,
var重复声明会被忽略;但在严格模式下,即使是var也会报错。
4.3 禁止删除不可删除的属性
'use strict';
delete Object.prototype; // TypeError
严格模式通过提前暴露潜在错误,帮助开发者写出更健壮的代码。
五、实战案例:解析复杂执行流程
让我们分析这段经典代码:
var a = 1;
function fn(a) {
console.log(a); // ?
var a = 2;
function a() {}
var b = a;
console.log(a); // ?
}
fn(3);
编译阶段(函数内部):
- 参数
a被初始化为3 - 函数声明
a()提升,覆盖参数a→a = function a() {} var a声明被忽略(已存在)var b提升为undefined
执行阶段:
console.log(a)→ 输出function a() {}var a = 2→ 赋值a = 2b = a→b = 2console.log(a)→ 输出2
最终输出:function a() {} 和 2。
这个例子完美展示了函数声明提升优先级高于参数和变量声明的规则。
六、最佳实践:写出符合机制的优雅代码
- 优先使用
let/const:避免var提升带来的意外 - 理解引用拷贝:修改对象时注意是否需要深拷贝
- 启用严格模式:尽早发现潜在错误
- 避免依赖提升:声明前置,提高代码可读性
- 函数参数视为只读:不要试图在函数内“替换”传入的对象
结语:机制即力量
JavaScript 的执行机制并非玄学,而是有着严谨逻辑的工程设计。从编译提升到内存分配,从调用栈到参数传递,每一个细节都服务于语言的灵活性与性能平衡。
当我们真正理解这些机制,就不再会被“奇怪”的输出所困扰,而是能够预见代码的行为,驾驭语言的特性,写出既高效又可靠的程序。