JavaScript 进阶核心知识点:垃圾回收、闭包、函数提升、剩余参数、展开运算符、对象解构
这几个是 JavaScript 进阶中的核心概念,直接影响代码的执行机制、性能和简洁性。下面从 定义、原理、用法、场景、注意事项 五个维度,逐一拆解每个知识点,结合实战示例帮你彻底理解。
一、垃圾回收(Garbage Collection, GC)
1. 核心定义
JavaScript 是自动垃圾回收语言,引擎会定期回收 "不再被引用" 的内存(如未使用的变量、对象),避免内存泄漏。开发者无需手动释放内存,但需理解其机制以避免不合理引用导致的内存浪费。
2. 垃圾回收原理
核心判断标准:对象是否可达(是否有变量、作用域链等引用它)。可达的对象会被保留,不可达的会被标记为 "垃圾",后续回收。
两种常见回收算法
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 标记 - 清除(主流) | 1. 遍历所有可达对象并标记;2. 清除未标记的对象;3. 整理内存碎片。 | 实现简单,适配复杂场景 | 回收后产生内存碎片 |
| 引用计数 | 记录对象被引用的次数,次数为 0 时回收。 | 实时回收,无需等待遍历 | 无法解决循环引用(如 a 引用 b,b 引用 a) |
注:现代浏览器(Chrome、Firefox)均使用 标记 - 清除算法,已解决循环引用问题(如闭包中的循环引用会被正确识别)。
3. 常见内存泄漏场景(需避免)
垃圾回收的 "坑" 多是 "意外保留可达引用" 导致,常见场景:
- 全局变量泛滥(未声明的变量默认挂载到
window/global,永久可达); - 闭包中意外保留大对象引用(如 DOM 元素、大型数组);
- 定时器 / 事件监听器未清除(
setInterval未用clearInterval取消,其回调函数及引用对象永久可达); - 未清理的 DOM 引用(如删除 DOM 元素后,仍有变量引用它)。
4. 优化建议
- 尽量避免全局变量,使用块级作用域(
let/const)替代var; - 定时器、事件监听器使用后及时清除(
clearInterval/removeEventListener); - 闭包中仅保留必要引用,避免挂载大对象;
- 手动解除无用引用(
obj = null),帮助引擎快速识别垃圾。
二、闭包(Closure)
1. 核心定义
闭包是函数及其声明时所在的词法作用域的组合。简单说:内层函数引用了外层函数的变量 / 参数,且内层函数被外层函数外部调用时,外层函数的作用域不会被销毁,内层函数依然能访问外层的变量 ------ 这个 "能访问外层变量的内层函数" 就是闭包。
2. 原理(基于词法作用域 + 作用域链)
- 词法作用域:函数的作用域在声明时就确定(而非调用时);
- 作用域链:函数执行时,会先在自身作用域找变量,找不到就向上遍历外层作用域,直到全局作用域;
- 闭包的关键:外层函数执行后,其作用域不会被垃圾回收(因为内层函数还在引用它的变量)。
3. 基础示例
javascript
运行
function outer() {
let count = 0; // 外层函数的局部变量
// 内层函数(闭包):引用了外层的 count
function inner() {
count++;
console.log(count);
}
return inner; // 外层函数返回内层函数,使其能在外部调用
}
const func = outer(); // 执行 outer,返回 inner(闭包)
func(); // 1(此时 outer 已执行完毕,但 count 仍被 inner 引用,未被回收)
func(); // 2(count 持续累加)
func(); // 3
4. 核心用途
- 封装私有变量 :JavaScript 没有原生私有变量,闭包可模拟 "私有属性 / 方法"(外部无法直接访问
count,只能通过闭包操作); - 保存状态:如计数器、缓存工具(闭包中保留的变量可跨多次调用共享状态);
- 延迟执行:定时器、事件回调中访问外层变量(如循环中保存循环变量)。
实用场景:模拟私有变量
javascript
运行
function createPerson(name) {
let age = 18; // 私有变量(外部无法直接修改)
return {
getName: () => name, // 闭包:访问外层 name
getAge: () => age, // 闭包:访问外层 age
grow: () => age++ // 闭包:修改外层 age
};
}
const person = createPerson("张三");
console.log(person.getName()); // 张三
console.log(person.getAge()); // 18
person.grow();
console.log(person.getAge()); // 19
console.log(person.age); // undefined(外部无法直接访问私有变量)
5. 注意事项
- 内存泄漏风险 :闭包会保留外层作用域,若闭包长期被引用(如挂载到全局),外层变量不会被回收,可能导致内存浪费。解决:不用时手动解除引用(
func = null); - 避免滥用:闭包会增加内存开销,非必要不使用(如简单场景无需刻意创建闭包);
- this 指向问题 :闭包中若用普通函数(非箭头函数),
this可能指向全局(浏览器中是window),需注意绑定this(如bind)。
三、函数提升(Function Hoisting)
1. 核心定义
函数提升是 JavaScript 变量提升的特殊情况:函数声明(Function Declaration)会被提升到当前作用域的顶部,允许在函数声明之前调用函数;而函数表达式(Function Expression)不会提升。
2. 原理
JavaScript 执行代码分两步:
- 编译阶段 :引擎扫描代码,将函数声明、变量声明(
var)提升到作用域顶部(变量提升仅提升声明,不提升赋值;函数声明提升整个函数体); - 执行阶段:按提升后的顺序执行代码。
3. 关键区别:函数声明 vs 函数表达式
(1)函数声明(会提升)
javascript
运行
// 调用在声明之前(有效)
foo(); // 输出:"函数声明被调用"
// 函数声明
function foo() {
console.log("函数声明被调用");
}
提升后的执行顺序:
javascript
运行
// 编译阶段:函数声明被提升到顶部
function foo() {
console.log("函数声明被调用");
}
// 执行阶段:调用函数
foo();
(2)函数表达式(不会提升)
javascript
运行
// 调用在表达式之前(报错:foo is not a function)
foo();
// 函数表达式(赋值给变量 foo)
var foo = function() {
console.log("函数表达式被调用");
};
提升后的执行顺序:
javascript
运行
// 编译阶段:仅变量 foo 被提升,赋值 undefined
var foo;
// 执行阶段:调用 foo(此时 foo 是 undefined,非函数)
foo(); // 报错
// 赋值阶段:foo 才指向函数
foo = function() {
console.log("函数表达式被调用");
};
(3)箭头函数(属于表达式,不会提升)
javascript
运行
bar(); // 报错:bar is not a function
const bar = () => console.log("箭头函数被调用");
4. 注意事项
-
函数提升优先级高于变量提升:若同时有同名变量声明和函数声明,函数声明会覆盖变量声明(但变量赋值后会覆盖函数); javascript
运行
console.log(foo); // 输出函数体(函数提升优先级更高) var foo = "变量"; function foo() { console.log("函数"); } console.log(foo); // 输出:"变量"(赋值后覆盖函数) -
块级作用域(
let/const)中,函数声明不会提升到块级作用域顶部,而是提升到 "块级作用域的临时死区(TDZ)",在声明前调用会报错;javascript
运行
if (true) { foo(); // 报错:Cannot access 'foo' before initialization let foo = function() {}; // 块级作用域中的函数表达式(let 声明) } -
避免依赖提升:建议将函数声明放在作用域顶部,或使用函数表达式(配合
let/const),使代码执行顺序更清晰。
四、剩余参数(Rest Parameters)
1. 核心定义
剩余参数(...参数名)允许函数接收任意数量的尾部参数 ,并将其转为数组。用于替代 arguments,解决其 "伪数组、不清晰" 的问题。
2. 语法与基础用法
javascript
运行
// 剩余参数 ...nums 接收所有尾部参数,转为数组
function sum(base, ...nums) {
console.log(base); // 固定参数(第一个参数)
console.log(nums); // 剩余参数(数组形式)
return base + nums.reduce((total, num) => total + num, 0);
}
console.log(sum(10, 20, 30)); // 60(10 + 20 + 30)
console.log(sum(5, 1, 2, 3)); // 11(5 + 1 + 2 + 3)
console.log(sum(0)); // 0(无剩余参数,nums 为空数组)
3. 关键规则
-
剩余参数必须放在所有参数的最后一位 (否则报错);
javascript
运行
// 错误:剩余参数不能在固定参数之前 function func(...rest, a, b) {} // SyntaxError -
一个函数只能有一个剩余参数;
-
剩余参数是真正的数组 (可直接使用
map/filter/reduce等数组方法),而arguments是伪数组(需转换)。
4. 与 arguments 的对比
| 特性 | 剩余参数 | arguments |
|---|---|---|
| 数据类型 | 真正的数组 | 伪数组(需 Array.from 转换) |
| 可读性 | 清晰(明确参数用途) | 模糊(需遍历判断) |
| 箭头函数支持 | 支持(箭头函数无 arguments) | 不支持(箭头函数无 arguments) |
| 参数筛选 | 可只接收尾部参数 | 接收所有参数,需手动筛选 |
示例:箭头函数中使用剩余参数
javascript
运行
// 箭头函数无 arguments,剩余参数是唯一选择
const multiply = (...nums) => nums.reduce((t, n) => t * n, 1);
console.log(multiply(2, 3, 4)); // 24
5. 实用场景
- 接收任意数量的参数(如求和、求积、拼接数组);
- 解构函数参数(配合对象 / 数组解构,灵活接收参数);
- 转发参数(将函数接收的参数原样转发给另一个函数)。
场景:转发参数
javascript
运行
// 封装 console.log,添加前缀
function logWithPrefix(prefix, ...args) {
console.log(`[${prefix}]`, ...args); // 剩余参数展开后转发给 console.log
}
logWithPrefix("INFO", "用户登录", "用户名:张三");
// 输出:[INFO] 用户登录 用户名:张三
五、展开运算符(Spread Syntax)
1. 核心定义
展开运算符(...)与剩余参数语法相同,但作用相反:将数组 / 对象 "打散" 为独立项,用于数组拼接、对象合并、函数传参等场景。
2. 两大核心用法
(1)数组展开(ES6)
- 数组拼接(替代
concat); - 数组浅拷贝(替代
slice()); - 函数传参(将数组转为独立参数);
- 类数组转数组(如
NodeList、arguments)。
javascript
运行
// 1. 数组拼接
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2, 7]; // [1,2,3,4,5,6,7]
// 2. 浅拷贝
const copy = [...arr1]; // [1,2,3](修改 copy 不影响 arr1,嵌套对象除外)
// 3. 函数传参
const nums = [10, 20, 30];
const max = Math.max(...nums); // 30(等价于 Math.max(10,20,30))
// 4. 类数组转数组
const lis = document.querySelectorAll("li"); // NodeList(伪数组)
const liArr = [...lis]; // 转为数组,可使用 map/filter
(2)对象展开(ES2018)
- 对象浅拷贝(替代
Object.assign); - 对象合并与属性覆盖(后面的对象覆盖前面同名属性);
- 新增 / 修改对象属性(不影响原对象)。
javascript
运行
// 1. 浅拷贝
const user = { name: "张三", age: 20 };
const userCopy = { ...user }; // { name: "张三", age: 20 }
// 2. 合并与覆盖
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = { ...obj1, ...obj2 }; // { a:1, b:3, c:4 }(b 被覆盖)
// 3. 新增/修改属性
const updatedUser = { ...user, age: 25, gender: "男" };
// { name: "张三", age:25, gender: "男" }
3. 注意事项
-
展开运算符是浅拷贝 :嵌套数组 / 对象仍为引用,修改会影响原数据(深拷贝需用
JSON.parse(JSON.stringify())或专门工具); -
对象展开仅包含自身可枚举属性(继承的属性、不可枚举属性不会展开);
-
数组展开可用于任何可迭代对象(如
String、Set、Map):javascript
运行
const str = "hello"; console.log([...str]); // ['h','e','l','l','o'](字符串转为数组) const set = new Set([1, 2, 3]); console.log([...set]); // [1,2,3](Set 转为数组)
六、对象解构(Object Destructuring)
1. 核心定义
对象解构是一种简洁的赋值语法:从对象中提取属性,并直接赋值给变量 ,避免重复写 obj.xxx。
2. 基础用法
(1)基本解构
javascript
运行
const user = { name: "张三", age: 20, gender: "男" };
// 解构:提取 name 和 age 属性,赋值给同名变量
const { name, age } = user;
console.log(name); // 张三
console.log(age); // 20
(2)重命名变量(避免变量名冲突)
javascript
运行
// 提取 gender 属性,重命名为 sex
const { gender: sex } = user;
console.log(sex); // 男
console.log(gender); // undefined(原变量名失效)
(3)设置默认值(属性不存在时使用默认值)
javascript
运行
// 提取 address 属性(不存在),默认值为 "未知"
const { address = "未知" } = user;
console.log(address); // 未知
// 若属性存在但值为 undefined,也会使用默认值
const obj = { a: undefined, b: null };
const { a = 1, b = 2 } = obj;
console.log(a); // 1(a 是 undefined,用默认值)
console.log(b); // null(b 存在且为 null,不用默认值)
(4)嵌套对象解构
javascript
运行
const user = {
name: "张三",
info: {
height: 180,
hobby: ["篮球", "游戏"]
}
};
// 解构嵌套属性:提取 info 中的 height 和 hobby
const { info: { height, hobby } } = user;
console.log(height); // 180
console.log(hobby); // ['篮球', '游戏']
3. 实用场景
(1)函数参数解构(简化参数接收)
javascript
运行
// 传统写法:需要手动提取属性
function printUser(user) {
const name = user.name;
const age = user.age;
console.log(`姓名:${name},年龄:${age}`);
}
// 解构写法:直接在参数中提取属性
function printUser({ name, age }) {
console.log(`姓名:${name},年龄:${age}`);
}
printUser({ name: "李四", age: 22 }); // 姓名:李四,年龄:22
(2)解构时设置参数默认值
javascript
运行
// 若未传参数,默认值为 {};若传参数但无 age,默认值为 18
function printUser({ name, age = 18 } = {}) {
console.log(`姓名:${name},年龄:${age}`);
}
printUser({ name: "王五" }); // 姓名:王五,年龄:18(age 用默认值)
printUser(); // 姓名:undefined,年龄:18(未传参数,用默认空对象)
(3)提取动态属性(属性名不确定时)
javascript
运行
const obj = { a: 1, b: 2, c: 3 };
const key = "b"; // 动态属性名
// 解构动态属性:用 [] 包裹变量
const { [key]: value } = obj;
console.log(value); // 2(等价于 obj[key])
4. 注意事项
-
解构赋值的变量必须提前声明(或用
let/const声明),否则报错;javascript
运行
// 错误:未声明变量直接解构 { name, age } = user; // SyntaxError(需加括号或声明变量) // 正确:用 let 声明,或加括号(针对已声明变量) let name, age; ({ name, age } = user); // 已声明变量解构,需加括号 -
解构嵌套对象时,若外层属性不存在,会报错(需设置默认值避免): javascript
运行
const { info: { weight } } = user; // 正确(info 存在) const { other: { x } } = user; // 报错:Cannot destructure property 'x' of 'undefined'
总结:核心知识点关联与应用
这六个知识点并非孤立,实际开发中常组合使用:
- 闭包 + 剩余参数:封装带状态的工具函数(如缓存函数);
- 展开运算符 + 对象解构:简化组件传参(如 React 组件 props 解构);
- 函数提升 + 剩余参数:编写灵活的工具函数;
- 垃圾回收:理解闭包、定时器的内存影响,避免内存泄漏。