JavaScript 执行机制与内存管理详解:从作用域链到垃圾回收
在 JavaScript 开发中,理解变量作用范围、内存分配与回收是解决复杂 Bug 的关键。本文详细拆解 JS 的执行上下文栈、作用域链查找规则,并对比讲解引用计数与标记清除两种垃圾回收算法,帮助掌握底层执行原理。
执行上下文与作用域链
JavaScript 在执行代码时,会维护一个执行上下文栈(Execution Context Stack,简称 ECStack)。无论是全局代码还是函数调用,都会创建对应的执行上下文(Global Execution Context 或 Function Execution Context)并按顺序压入栈中。每个执行上下文都包含一个变量对象(Variable Object),用于存储该作用域内的变量和函数声明。
全局执行上下文的创建
当脚本开始运行时,会创建全局执行上下文(GEC)并压入栈底。此时存在一个全局对象(GlobalObject,浏览器环境中为 window),GO 的属性对应全局变量。
- 编译阶段: GO 会对全局定义的变量初始化为
undefined;若遇到函数声明,则以函数名为属性名,值为指向函数代码段的内存地址,同时记录该函数的父级作用域。 - 执行阶段: 对变量进行赋值。此阶段还会注册全局内置对象和函数(如
setTimeout,Date,String等),并将window赋值为this。
函数执行上下文的创建
当执行流进入函数体内时,会创建函数执行上下文(FEC)并压入栈顶。函数内部同样拥有自己的变量对象,在函数执行上下文中,VO 通常指向活动对象(Activation Object,AO)。
- 参数初始化: 形参作为 AO 的属性,实参传入值;若无实参则初始化为
undefined。 - 变量提升: 使用
var声明的变量会被添加至 AO 并赋值为undefined;函数声明会被提升至当前作用域顶部。 - 代码执行: 实际执行赋值语句,完成变量的最终赋值。
作用域链的查找机制
每个执行上下文都会创建一个作用域链,用于变量查找。作用域链由当前的 VO/AO 及外层(父级)作用域的 VO 链接而成。当 JavaScript 引擎查找变量时,会沿着这条链条从内向外搜索,直到找到变量或抵达全局作用域为止。
function foo() {
var a = b = 10;
}
foo();
console.log(a, b); // ReferenceError, 10var a = b = 10 等效于 b = 10; var a = b;。由于 b 未加修饰符,它被隐式声明为全局变量加入 GO,而 a 仅在局部作用域有效,因此全局输出 a 时会报错。
此外,需注意变量提升带来的“先使用后定义”现象。例如在函数编译阶段已发现 return var a = 100,引擎会认为 a 已定义但值为 undefined,若在此行之前访问 a,得到的将是 undefined 而非报错。
内存管理与垃圾回收
程序运行需要占用内存,其生命周期通常包括申请(分配)、使用和释放三个阶段。与 C/C++ 等手动管理语言的 malloc/free 不同,JavaScript 通过自动垃圾回收机制(Garbage Collection)来管理内存。
引用计数算法
这种算法的基本逻辑是跟踪每个值被引用的次数。当一个值被引用一次时,计数器 +1;当不再引用时 -1。当计数器归零时,说明该值无法再被访问,垃圾回收器会在下一次运行时清理它。
该算法的主要缺陷在于循环引用会导致内存泄漏:
var obj1 = { info: obj2 };
var obj2 = { info: obj1 }; // 引用计数永远不为 0标记清除算法
目前主流的 JS 引擎(如 V8)均采用标记清除(Mark-and-Sweep)算法。该算法将堆内存中的根对象(通常是全局执行上下文)设为起始点,定期扫描所有可到达的对象并将其标记为“活跃”。对于那些无法从根对象到达的“不可达”对象,则被判定为垃圾并进行回收。
该方法能有效解决循环引用导致的内存泄漏问题,是目前前端性能优化中预防内存问题的核心理论基础。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









