JavaScript进阶篇——第一章 作用域与垃圾回收机制
目录
一、作用域基础
二、局部作用域
三、全局作用域
四、作用域链
五、垃圾回收机制
六、内存生命周期
七、垃圾回收算法
作用域决定了变量的可访问范围,分为全局作用域(程序全局可访问)和局部作用域(函数或块内有效)。局部作用域包含函数作用域和块作用域,其中let
/const
具有块作用域,而var
仅函数作用域且会变量提升。作用域链机制允许子作用域访问父作用域变量,反之则不行。
垃圾回收(GC)自动管理内存,现代标记清除算法从全局对象扫描并回收不可达内存(包括循环引用),淘汰的引用计数法则无法处理循环引用。内存泄漏常见于未清除的定时器、事件监听或意外全局变量,需手动解除引用(如bigData = null
)。
核心区别:
let
/const
:块作用域,无变量提升。var
:函数作用域,会提升。- 内存优化:避免全局变量,及时清理资源。
一、作用域基础
核心概念
作用域规定了变量可被访问的范围,超出范围则无法访问
作用域类型
类型 | 范围 | 特点 |
---|---|---|
全局作用域 | 整个程序 | 任何地方都可访问 |
局部作用域 | 函数内部或代码块内部 | 仅内部可访问 |
作用域关系图解
二、局部作用域
1. 函数作用域
在函数内部声明的变量,只能在函数内部访问
function calculate() {// 函数作用域变量const taxRate = 0.1;console.log(taxRate); // ✅ 可访问
}calculate();
console.log(taxRate); // ❌ 报错:未定义
2. 块作用域
在
{}
代码块中声明的变量,只能在块内访问
{// 块作用域变量const temp = 25;console.log(temp); // ✅ 可访问
}console.log(temp); // ❌ 报错:未定义// if/for循环中的块作用域
for(let i=0; i<3; i++) {console.log(i); // ✅ 0,1,2
}
console.log(i); // ❌ 报错:未定义
⚠️ 关键特性
声明方式 | 函数作用域 | 块作用域 | 特点 |
---|---|---|---|
var | ✅ | ❌ | 会提升,无块作用域 |
let | ✅ | ✅ | 块级作用域 |
const | ✅ | ✅ | 块级作用域,常量 |
三、全局作用域
全局变量定义
// 全局作用域变量
const APP_NAME = 'MyApp';
let userCount = 0;function registerUser() {userCount++; // ✅ 可访问全局变量console.log(`${APP_NAME} 用户数:${userCount}`);
}
三种全局定义方式
// 1. 显式声明(推荐)
const globalVar = 'value';// 2. 动态添加window属性(不推荐)
window.dynamicProp = 'unsafe';// 3. 函数内未声明变量(危险!)
function leakGlobal() {leakedVar = '污染全局'; // ❌ 自动成为全局变量
}
❗ 最佳实践
最小化使用全局变量
避免使用
var
声明全局变量使用模块化封装代码
四、作用域链
核心机制
代码示例
// 全局作用域
const global = '全局';function outer() {// outer作用域const outerVar = '外部';function inner() {// inner作用域const innerVar = '内部';console.log(innerVar); // "内部"(当前作用域)console.log(outerVar); // "外部"(父作用域)console.log(global); // "全局"(全局作用域)}inner();
}outer();
关键规则
-
由内向外查找变量
-
子作用域可访问父作用域变量
-
父作用域无法访问子作用域变量
-
同级作用域变量不可互访
五、垃圾回收机制
核心概念
垃圾回收(GC):自动内存管理机制,回收不再使用的内存空间
回收对象
对象类型 | 回收时机 | 示例 |
---|---|---|
局部变量 | 函数执行完毕 | 函数内部变量 |
全局变量 | 页面关闭时 | 页面级全局数据 |
未引用对象 | 无任何引用指向时 | obj = null 后的对象 |
循环引用 | 现代GC算法可处理 | 相互引用的孤立对象 |
六、内存生命周期
三阶段模型
各阶段详解
-
内存分配
// 基本类型(栈内存) const age = 25; // 引用类型(堆内存) const user = { name: 'John' };
-
内存使用
console.log(age); // 读取 user.age = 30; // 修改
-
内存回收
// 局部变量自动回收 function process() {const temp = new Array(1000);// 函数结束自动回收 }// 手动解除引用 let bigData = loadHugeData(); bigData = null; // 标记为可回收
内存泄漏案例
// 1. 未清理的定时器
setInterval(() => {// 持续占用内存
}, 1000);// 2. 未移除的事件监听
element.addEventListener('click', handler);// 3. 意外的全局变量
function createLeak() {leaked = '全局变量'; // 忘记声明
}
七、垃圾回收算法
1. 引用计数法(淘汰)
原理:跟踪每个值被引用的次数
let a = { x: 1 }; // 引用计数=1
let b = a; // 引用计数=2a = null; // 引用计数=1
b = null; // 引用计数=0 → 回收
致命问题:循环引用
function createCycle() {let o1 = {};let o2 = {};o1.ref = o2; // o1引用o2o2.ref = o1; // o2引用o1// 引用计数永远为1,无法回收
}
2. 标记清除法(现代主流)
原理:从根部(全局对象)扫描,标记可达对象
执行过程:
-
从
window
对象开始扫描 -
标记所有可达对象
-
清除未标记对象
优势:
-
可处理循环引用
-
高效回收大内存块
-
现代浏览器优化算法(分代回收等)
✅ 核心要点总结
📝 高频面试题速答
-
Q:let/const/var的作用域区别?
A:let/const有块作用域,var只有函数作用域
-
Q:什么是作用域链?
A:变量查找机制,从当前作用域向父级作用域逐级查找
-
Q:垃圾回收的两种主要算法?
A:引用计数法(已淘汰)和标记清除法(主流)
-
Q:如何避免内存泄漏?
A:及时清除定时器、移除事件监听、避免意外全局变量
-
Q:为什么循环引用不会导致内存泄漏?
A:现代标记清除法可检测并回收不可达的循环引用对象
🧠 记忆口诀
"作用域分内外,回收标记不可达"
内外:全局作用域和局部作用域
标记:标记清除法从根部扫描