JavaScript进阶篇垃圾回收、闭包、函数提升、剩余参数、展开运算符、对象解构

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 执行代码分两步:

  1. 编译阶段 :引擎扫描代码,将函数声明、变量声明(var)提升到作用域顶部(变量提升仅提升声明,不提升赋值;函数声明提升整个函数体);
  2. 执行阶段:按提升后的顺序执行代码。

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());
  • 函数传参(将数组转为独立参数);
  • 类数组转数组(如 NodeListarguments)。

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()) 或专门工具);

  • 对象展开仅包含自身可枚举属性(继承的属性、不可枚举属性不会展开);

  • 数组展开可用于任何可迭代对象(如 StringSetMap):

    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 解构);
  • 函数提升 + 剩余参数:编写灵活的工具函数;
  • 垃圾回收:理解闭包、定时器的内存影响,避免内存泄漏。
相关推荐
czhc11400756633 小时前
C# 1116 流程控制 常量
开发语言·c#
程序员ys3 小时前
Vue的响应式系统是怎么实现的
前端·javascript·vue.js
aduzhe3 小时前
关于在嵌入式中打印float类型遇到的bug
前端·javascript·bug
程序定小飞3 小时前
基于springboot的汽车资讯网站开发与实现
java·开发语言·spring boot·后端·spring
鹏多多4 小时前
vue过滤器filter的详解及和computed的区别
前端·javascript·vue.js
孟陬4 小时前
在浏览器控制台中优雅地安装 npm 包 console.install('lodash')
javascript·node.js
Moment4 小时前
LangChain 1.0 发布:agent 框架正式迈入生产级
前端·javascript·后端
大米粥哥哥4 小时前
Qt 使用QAMQP连接RabbitMQ
开发语言·qt·rabbitmq·qamqp
yivifu4 小时前
精益求精,支持处理嵌套表格的Word表格转HTML表格
开发语言·c#·word