JavaScript 面试题

1. 闭包是什么?利弊?如何解决弊端?

极简口述总结(面试背诵)

闭包是内层函数引用外层函数变量,并在外部执行形成的作用域环境

优点:封装私有变量、缓存、柯里化、保持状态

缺点:变量无法自动回收,易内存泄漏,查找作用域性能差

解决方案:用完手动置空引用、清除定时器 / 事件、缩短闭包生命周期、减少不必要的闭包

什么是闭包

  标准定义:闭包 = 内层函数 + 内层函数对外层函数作用域变量的引用

  简单说:函数 A 内部有函数 B,B 访问了 A 的局部变量,B 被外部执行,就形成闭包

  口述面试:当一个内部函数引用了外部函数的局部变量,且内部函数在外部函数执行完后仍能被访问调用,这个内部函数连同它所持有的外层作用域环境,就称为闭包

形成闭包的3个必要条件:(面试必背)

  • 存在函数嵌套(内层函数、外层函数)
  • 内层函数引用外层函数的局部变量 / 参数
  • 内层函数在外层函数作用域之外被调用

基础示例

 1 function outer() {
 2   let count = 0; // 自由变量
 3   // 内层函数,引用count
 4   function inner() {
 5     count++;
 6     console.log(count);
 7   }
 8   return inner; // 返回内层函数
 9 }
10 const fn = outer(); 
11 fn(); // 1
12 fn(); // 2

闭包的好处(用途,面试举例)

  • 保存私有变量,实现模块化、封装:外部无法直接修改内部变量,只能通过暴露的方法操作,数据安全
 1 function person() {
 2   let age = 18; // 私有变量
 3   return {
 4     getAge: () => age,
 5     addAge: () => age++
 6   }
 7 }
 8 const p = person();
 9 console.log(p.getAge()); // 18
10 p.addAge();
11 console.log(p.getAge()); // 19
12 // p.age 访问不到
  • 变量持久化,缓存数据:计数器、节流防抖缓存、单例、缓存函数结果
  • 实现柯里化、函数偏应用:拆分参数,延迟执行
1 function add(a) {
2   return b => a + b;
3 }
4 const add2 = add(2);
5 add2(3); // 5
  • 解决循环定时器 / 事件绑定变量捕获问题
 1 for(var i = 0; i < 3; i++){
 2   setTimeout(()=>console.log(i), 1000)
 3 }
 4 // 输出3,3,3 无闭包问题
 5 // 用闭包解决
 6 for(var i = 0; i < 3; i++){
 7   (function(j){
 8     setTimeout(()=>console.log(j),1000)
 9   })(i)
10 }
11 // 0,1,2
  • 模拟块级作用域(ES5 无 let/const 时代):立即执行函数 IIFE + 闭包创立独立作用域

闭包的弊端(核心考点)

  • 内存泄漏 / 内存占用过高:闭包会持有外层作用域,自由变量不会被 GC 垃圾回收,大量创建闭包、长期不销毁会持续占用堆内存
  • 容易造成内存泄漏场景
    • 闭包绑定 DOM 元素,页面销毁时未解除引用
1 function bindBtn() {
2   const btn = document.getElementById('btn');
3   let num = 1;
4   btn.onclick = () => {
5     num++;
6   }
7 }
8 // 页面卸载时,btn、num永远无法回收
    • 全局长期存储闭包函数,持续引用大量数据
    • 定时器、事件监听使用闭包,忘记清除定时器 / 解绑事件
  • 变量污染、难以调试:私有变量无法直接打印查看,多层闭包变量追踪复杂;多人协作易出现意外修改自由变量
  • 性能耗损:作用域链查找变量比普通函数慢;大量闭包会增加 GC 负担

如何解决闭包带来的弊端(面试官必追问)

  • 主动切断引用,手动释放内存:将闭包引用的变量、DOM、定时器置为 null,断开引用链,让 GC 回收
 1 function bindBtn() {
 2   const btn = document.getElementById('btn');
 3   let num = 1;
 4   function click() {
 5     num++;
 6   }
 7   btn.onclick = click;
 8 
 9   // 页面销毁时执行清除
10   function clear() {
11     btn.onclick = null; // 解绑事件
12     num = null;
13   }
14   return clear;
15 }
  • 及时清除定时器、事件监听
    • clearTimeout / clearInterval 清除计时器
    • removeEventListener 解绑 DOM 事件
  • 避免全局长期保存闭包实例:不要将闭包函数挂载在全局变量长期持有,用完及时置空
let fn = outer();
fn();
fn = null; // 释放闭包
  • 合理控制闭包生命周期:临时一次性场景使用,执行完毕立即销毁,不要长期缓存闭包
  • 尽量使用 ES6 let/const、块级作用域替代冗余闭包:ES6 有块级作用域,很多 ES5 需要 IIFE 闭包的场景可以直接替换,减少闭包使用
  • 模块化规范管理私有变量:使用 ESModule、class 私有属性 #、模块化方案,减少多层嵌套闭包
  • 调试:Chrome 内存面板排查泄漏:Memory 面板快照,查找未回收的闭包作用域、DOM 引用,定位内存泄漏

2. 深拷贝和浅拷贝的区别

极简口述总结

浅拷贝只复制第一层,嵌套引用共享内存,修改深层会污染原数据;

深拷贝递归复制全部层级,生成全新独立对象,修改互不影响;

浅拷贝性能好但有数据污染风险,深拷贝数据安全但性能开销更大。

核心底层原理

JS 数据分两类:

  • 基本数据类型:String/Number/Boolean/null/undefined/Symbol/BigInt,存栈内存,赋值直接复制值
  • 引用数据类型:Object/Array/Function/Date 等,栈存地址指针,堆存真实数据

浅拷贝:只复制一层栈地址,新对象和原对象共用堆里的深层数据;只拷贝第一层属性,嵌套引用类型还是共享同一个堆地址

深拷贝:递归复制所有层级,完全开辟新堆内存;新对象、原对象彻底隔离,修改任意一方互不影响

两者区别(面试分点背诵)

维度 浅拷贝 深拷贝
拷贝层级 仅拷贝第一层属性 递归拷贝所有嵌套层级
内存关系 引用类型共用堆内存地址 全部开辟全新堆内存,完全独立
修改影响 修改嵌套对象,原对象同步变化 无论改多少层,互不干扰
性能 速度快、开销小 递归遍历,大数据性能较差
适用场景 简单单层对象,无嵌套引用 多层嵌套对象、后端回显数据隔离

浅拷贝常见实现方式(面试要列举)

  • 展开运算符 ...
  • Object.assign({}, obj)
  • 数组方法:slice() / concat()
  • 手动循环遍历第一层赋值

共同缺陷:只能处理一层,嵌套引用会共享内存

深拷贝实现方式 + 优缺点(高频考点)

  • JSON.parse(JSON.stringify())
1 const newObj = JSON.parse(JSON.stringify(oldObj))
    • 优点:写法极简,日常接口库数据常用
    • 缺点(面试必说坑)
      • 无法拷贝 undefined、Symbol、函数,直接丢失
      • 日期会转为字符串、正则 / Error 对象丢失特性
      • 存在循环引用直接报错
      • 无法拷贝 Map、Set
  • 递归手写深拷贝(面试官最爱让手写)

核心逻辑:

    • 判断类型:基础类型直接返回
    • 区分数组 / 对象,创建空容器
    • 循环遍历键,递归赋值
 1 function deepClone(obj) {
 2   // 基础类型/ null 直接返回
 3   if (obj === null || typeof obj !== 'object') return obj;
 4   // 区分数组、对象
 5   const target = Array.isArray(obj) ? [] : {};
 6   // 递归拷贝每一个属性
 7   for (let key in obj) {
 8     if (obj.hasOwnProperty(key)) {
 9       target[key] = deepClone(obj[key]);
10     }
11   }
12   return target;
13 }

优化扩展:兼容 Date、RegExp、处理循环引用、Map/Set

  • structuredClone(浏览器原生 API,ES2022)
1 const copy = structuredClone(obj)
    • 优点:原生支持 Date、RegExp、Map、Set,可处理循环引用
    • 缺点:不支持函数、DOM 元素,低版本浏览器不兼容
  • 第三方库 Lodash _.cloneDeep():生产项目常用,兼容性强,处理边界完整

3. 实现拷贝的方法有哪些?

浅拷贝:数组可以用拓展运算符 [ ...arr ],或者 slice();对象可以用 Object.assign((), obj)

深拷贝:JSON.parse(JSON.stringify(obj)),或封装递归方法,或使用第三方库的方法,比如JQuery的$.extend({}, obj),或者lodash的cloneDeep

4. JSON.parse(JSON.stringify(obj)) 处理的缺点

  • 如果对象中有属性是function或undefined,处理后会被过滤掉
  • 如果属性值是对象,且由构造函数生成的实例对象,会丢弃对象的constructor

5. 当执行 setTimeout、new Promise、fetch 时,JS引擎、Web APIs、调用栈、宏任务队列、微任务队列是如何协同工作的?为什么需要事件循环机制?

JS 主线程拥有调用栈,异步操作交给浏览器 Web APIs 多线程处理,任务完成后回调进入对应队列,由事件循环调度。

  • setTimeout(宏任务):执行到 setTimeout 时,回调函数交给 Web APIs 定时器线程计时。计时完成后,回调被推入宏任务队列。等到调用栈清空、所有微任务执行完毕,事件循环才会取出该回调执行。
  • new Promise:Promise 构造器内的函数是同步代码,直接在调用栈执行。调用 resolve/reject 后,只会把.then.catch回调放入微任务队列,不进入 Web APIs。当前同步代码执行完,会一次性清空全部微任务队列。
  • fetch(网络请求,宏任务):fetch 网络请求交由 Web APIs 网络线程异步发起,主线程不会阻塞。后端数据返回后,成功回调被加入宏任务队列,等待本轮微任务全部执行结束后再运行。

整体协作流程

  • 先执行全局同步代码,同步函数入栈执行。
  • 遇到异步方法:定时器、网络请求交给 Web APIs 后台执行;Promise 的 then 回调存入微任务队列。
  • 调用栈清空后,优先一次性执行完所有微任务。
  • 再从宏任务队列取出一个宏任务放入调用栈执行。
  • 每执行完一个宏任务,再次清空新增微任务,如此循环,这就是事件循环。
执行优先级:同步代码全部微任务单个宏任务
为什么需要事件循环?
  • JavaScript 是单线程语言,为了防止多线程同时操作 DOM 引发冲突,主线程只能串行执行代码。
  • 如果耗时的定时器、网络请求同步等待,会阻塞页面渲染与用户交互。因此把耗时操作交给浏览器多线程 Web APIs 后台运行,实现非阻塞。
  • 后台线程完成任务后不能随意打断主线程代码。事件循环作为调度机制,保证主线程空闲时再有序执行异步回调,兼顾执行顺序与运行效率。

极简口述版:JS 主线程维护调用栈,耗时异步操作交给浏览器 Web APIs。setTimeout 和 fetch 完成后回调进入宏任务队列,Promise.then 进入微任务队列。事件循环规则:先跑完所有同步代码,栈空后清空全部微任务,再执行一个宏任务,不断循环。

之所以要有事件循环,是因为 JS 单线程避免 DOM 竞争冲突,同时借助浏览器多线程实现异步非阻塞,依靠事件循环有序调度所有异步回调,防止主线程阻塞卡死。

6. 如何判断空对象?如何区分数据类型?

判断空对象

用JSON的stringify转成字符串后,跟‘{}’对比;

用ES6,判断Object.keys(targetObject)返回值数组的长度是否为0;

用ES5,判断Object.getOwnPropertyNames(targetObject)返回的数组

7. 闭包和作用域的关系?

作用域是静态代码结构,闭包是运行时产生的作用域环境,闭包延长了作用域生命周期

8. GC 垃圾回收为什么回收不了闭包变量?

GC 判断规则:是否存在有效引用。闭包函数内部持有自由变量的引用,引用不为空,变量就不会被回收

9. let/const 会形成闭包嘛?

会,只要内部函数引用外部块 / 函数变量并外部调用,就构成闭包,和 var 无关

10. class 私有属性是闭包嘛?

不是,# 私有属性是语法层面封装,不依赖函数作用域闭包实现

11. 赋值=、浅拷贝、深拷贝三者区别?

  • 直接赋值 let x = obj:完全共用同一个地址,改任意一层都会互相影响
  • 浅拷贝:第一层独立,深层共用
  • 深拷贝:所有层级完全独立

12. 什么时候用浅拷贝,什么时候用深拷贝

  • 浅拷贝:对象只有单层属性、简单数组,追求性能
  • 深拷贝:表单回显、表格编辑、多层嵌套树形数据,需要隔离原始数据防止污染

13. 手写递归深拷贝要考虑哪些边界?

null、基础类型、数组、对象、Date、正则、循环引用、Symbol 键、Map/Set

 

posted @ 2026-06-24 18:41  傅丹辰cds  阅读(4)  评论(0)    收藏  举报