JavaScript 全维度面试题

5 阅读16分钟

JavaScript 全维度面试题(附标准答案 + 考点分析)

以下覆盖 JavaScript 面试所有核心模块,按「基础语法→进阶原理→异步编程→ES6+→DOM/BOM→性能 / 安全→工程化→手写代码」分类,适配初级 / 中级 / 高级面试,每个问题标注「难度」和「核心考点」,既适合快速复习,也能应对深度追问。

一、基础语法(初级必掌握)

1. JS 有哪些数据类型?区别是什么?

难度:★☆☆标准答案

  • 基础类型(7 种):StringNumberBooleanNullUndefinedSymbol(ES6)、BigInt(ES10);

    • 特点:存储在栈内存,值不可变,赋值是 “值拷贝”;
  • 引用类型:Object(包含 ArrayFunctionDateRegExpSet/Map 等);

    • 特点:存储在堆内存,变量保存的是堆地址,赋值是 “引用拷贝”。

核心考点:区分栈 / 堆、值类型不可变(如 let str = 'a'; str += 'b' 是新建字符串)、Null 是 “空对象指针”(typeof null === 'object')。

2. 如何准确判断数据类型?

难度:★★☆标准答案

方法适用场景示例
typeof基础类型(除 Null)typeof '123' → 'string'typeof null → 'object'
instanceof引用类型(判断原型链)[] instanceof Array → true{} instanceof Object → true
Object.prototype.toString.call()所有类型(最准确)Object.prototype.toString.call(null) → '[object Null]'

核心考点typeof 的局限性(Null 判为 object、引用类型均判为 object)、instanceof 不能判断基础类型、万能判断方法的原理(利用 Symbol.toStringTag 定制)。

3. null 和 undefined 的区别?

难度:★☆☆标准答案

  • undefined:变量声明未赋值、函数无返回值、对象属性不存在时的默认值(JS 引擎自动赋值);
  • null:主动赋值,表示 “空对象指针”(如手动清空引用类型变量);
  • 对比:Number(undefined) → NaNNumber(null) → 0undefined === null → falseundefined == null → true

4. 闭包的定义、用途、优缺点?

难度:★★☆标准答案

  • 定义:函数嵌套时,内部函数引用外部函数的变量 / 参数,外部函数执行后,变量仍被内部函数保留(不被垃圾回收),形成闭包;

  • 核心用途:

    1. 私有化变量(如模块化:避免全局污染);
    2. 缓存数据(如计算结果缓存);
    3. 延长变量生命周期(如防抖节流);
  • 优点:模块化、数据私有;

  • 缺点:变量长期驻留内存,易导致内存泄漏(需手动释放:fn = null)。

示例

javascript

运行

function createCounter() {
  let count = 0; // 闭包保留的变量
  return () => count++;
}
const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1

5. 作用域和作用域链的原理?

难度:★★☆标准答案

  • 作用域:变量的可访问范围,分为「全局作用域」「函数作用域」「块级作用域(ES6 let/const)」;
  • 作用域链:变量查找规则 —— 当前作用域找不到变量时,向上级作用域查找,直到全局作用域;
  • 核心:作用域在定义时确定(词法作用域),而非执行时;作用域链保证变量的查找顺序。

6. this 指向的完整规则?

难度:★★★标准答案(优先级从高到低):

  1. new 绑定new 构造函数 → this 指向新创建的实例;
  2. 显式绑定call/apply/bind → this 指向绑定的对象;
  3. 隐式绑定obj.fn() → this 指向 obj
  4. 默认绑定:独立调用函数 → 全局环境(浏览器 window,Node global),严格模式下为 undefined
  5. 箭头函数:无自身 this,继承外层作用域的 this(无法被 call/apply/bind 修改)。

示例

javascript

运行

const obj = {
  fn: () => console.log(this) // 箭头函数继承外层 this(全局)
};
obj.fn(); // window(浏览器)

7. 浅拷贝 vs 深拷贝的区别?实现方式?

难度:★★☆标准答案

  • 浅拷贝:仅拷贝对象第一层属性,引用类型属性仍共享地址(修改拷贝后对象会影响原对象);实现:Object.assign、扩展运算符 {...obj}Array.slice/concat
  • 深拷贝:递归拷贝所有层级属性,新对象与原对象完全独立;实现:JSON.parse(JSON.stringify())(有局限)、递归手写、lodash.cloneDeep

核心考点JSON.parse(JSON.stringify()) 的局限性(无法拷贝函数 / Symbol / 循环引用 / Date)。

二、原型与继承(中级核心)

1. 原型链的核心原理?

难度:★★★标准答案

  • 每个函数(除箭头函数)有 prototype(原型对象),原型对象有 constructor 指向函数本身;
  • 每个对象(包括实例)有 __proto__ 指向其构造函数的 prototype
  • 原型链:对象访问属性时,自身找不到则通过 __proto__ 向上查找,直到 Object.prototype(顶端,__proto__ 为 null);
  • 核心:继承的本质是原型链查找。

2. 实现继承的常用方式?(从差到优)

难度:★★★标准答案

方式实现思路缺点
原型链继承Child.prototype = new Parent()父类引用属性被所有实例共享、无法传参
构造函数继承Parent.call(this, args)无法继承父类原型方法、方法在构造函数内定义(复用性差)
组合继承原型链 + 构造函数父类构造函数被调用两次(原型和实例各一次)
寄生组合继承(最优)原型链继承原型方法 + 构造函数继承属性无冗余,ES6 class extends 底层实现
ES6 class 继承class Child extends Parent { constructor() { super() } }语法糖,底层仍是原型链

寄生组合继承示例

javascript

运行

function Parent(name) { this.name = name; }
Parent.prototype.say = () => console.log('hello');

function Child(name, age) {
  Parent.call(this, name); // 继承属性
  this.age = age;
}
// 继承原型方法(避免调用父类构造函数)
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修正 constructor 指向

3. new 操作符的执行过程?

难度:★★★标准答案

  1. 创建一个空对象 obj
  2. obj.__proto__ = 构造函数.prototype(绑定原型);
  3. 执行构造函数,this 指向 obj(传参);
  4. 若构造函数返回引用类型,返回该值;否则返回 obj

手写 new

javascript

运行

function myNew(Fn, ...args) {
  const obj = Object.create(Fn.prototype);
  const result = Fn.apply(obj, args);
  return result instanceof Object ? result : obj;
}

4. instanceof 的实现原理?

难度:★★★标准答案

  • 原理:判断 右值.prototype 是否出现在 左值 的原型链上;
  • 手写实现:

javascript

运行

function myInstanceof(left, right) {
  if (typeof left !== 'object' || left === null) return false;
  let proto = Object.getPrototypeOf(left);
  while (proto) {
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

三、异步编程(高级高频)

1. 事件循环(Event Loop)的原理?(浏览器 / Node 区别)

难度:★★★★标准答案

  • 核心:JS 是单线程,通过事件循环实现异步,分为「调用栈」「宏任务队列」「微任务队列」;

  • 执行顺序:

    1. 执行同步代码(调用栈);
    2. 执行完同步代码后,清空所有微任务(按顺序);
    3. 执行一个宏任务,再清空所有微任务;
    4. 重复步骤 3,直到队列清空;
  • 任务类型:

    • 宏任务:script 整体、setTimeout/setIntervalDOM 事件AJAXsetImmediate(Node)
    • 微任务:Promise.then/catch/finallyasync/awaitMutationObserverprocess.nextTick(Node,优先级最高)
  • 浏览器 vs Node:Node 事件循环分 6 个阶段(timers、pending callbacks 等),微任务在 nextTick 阶段和 poll 阶段结束后执行。

示例(执行顺序:1→2→4→5→3):

运行

console.log(1);
setTimeout(() => console.log(3), 0);
Promise.resolve().then(() => console.log(4));
async function fn() { await console.log(2); console.log(5); }
fn();

2. Promise 的核心原理?手写 Promise 核心逻辑?

难度:★★★★标准答案

  • Promise 是异步编程解决方案,有 3 个状态:pendingfulfilled/rejected(状态不可逆);
  • 核心:then 方法支持链式调用,返回新 Promise,捕获错误冒泡;
  • 手写核心(简化版):

javascript

运行

class MyPromise {
  constructor(executor) {
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try { executor(resolve, reject); }
    catch (e) { reject(e); }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e; };
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.status === 'fulfilled') {
        setTimeout(() => { // 异步执行
          try {
            const x = onFulfilled(this.value);
            resolve(x); // 简化版,未处理 x 是 Promise 的情况
          } catch (e) { reject(e); }
        }, 0);
      }
      if (this.status === 'rejected') { /* 类似 fulfilled */ }
      if (this.status === 'pending') {
        this.onResolvedCallbacks.push(() => { /* 异步执行 onFulfilled */ });
        this.onRejectedCallbacks.push(() => { /* 异步执行 onRejected */ });
      }
    });
    return promise2;
  }
}

3. async/await 的原理?和 Promise 的关系?

难度:★★★标准答案

  • async/await 是 Promise 的语法糖,底层基于 Generator + 自动执行器;
  • 原理:async 函数返回 Promise,await 暂停函数执行,等待 Promise 状态变更后恢复执行;
  • 优势:解决 Promise 链式调用的 “回调地狱”,代码更同步化;
  • 注意:await 后若不是 Promise,会被包装为 Promise.resolve(值)await 错误需用 try/catch 捕获。

4. 回调地狱的解决方式?

难度:★★☆标准答案(从差到优):

  1. 嵌套回调(地狱,不推荐);
  2. Promise 链式调用(then 串联);
  3. async/await(最优,同步化代码);
  4. Generator + 执行器(如 co 库,async/await 的前身)。

5. 如何实现 Promise.all/Promise.race/Promise.allSettled?

难度:★★★标准答案

  • Promise.all:接收 Promise 数组,全部成功返回结果数组,一个失败立即返回错误;手写:

javascript

运行

function myAll(promises) {
  return new Promise((resolve, reject) => {
    const res = [];
    let count = 0;
    promises.forEach((p, i) => {
      Promise.resolve(p).then(val => {
        res[i] = val;
        count++;
        if (count === promises.length) resolve(res);
      }, err => reject(err));
    });
  });
}
  • Promise.race:返回第一个完成的 Promise(成功 / 失败均可);
  • Promise.allSettled:等待所有 Promise 完成(无论成功 / 失败),返回每个 Promise 的状态和结果。

四、ES6+ 新特性(全级别高频)

1. let/const 与 var 的区别?

难度:★☆☆标准答案

特性varlet/const
作用域函数作用域块级作用域
变量提升有(可先使用后声明)有(但处于 “暂时性死区”,不可先使用)
重复声明允许不允许
初始值可选const 必须初始化
绑定全局对象是(挂载到 window)

2. 箭头函数与普通函数的区别?

难度:★★☆标准答案

  1. 无自身 this,继承外层作用域的 this(无法被 call/apply/bind 修改);
  2. 无 arguments 对象(需用剩余参数 ...args 替代);
  3. 不能作为构造函数(不能 new);
  4. 无 prototype 属性;
  5. 语法更简洁,适合回调函数(如数组方法)。

3. 解构赋值的用途?

难度:★☆☆标准答案

  • 数组解构:const [a, b] = [1, 2](按位置取值);
  • 对象解构:const { name: n } = { name: '张三' }(按键取值,可重命名);
  • 用途:简化变量赋值、函数参数默认值、解构返回值(如 const { data } = await fetch())。

4. Set/Map 的用途?和数组 / 对象的区别?

难度:★★☆标准答案

  • Set:无序不重复的集合,用于数组去重、判断元素是否存在;优势:has() 查找效率 O (1)(数组 includes() 是 O (n));
  • Map:键值对集合(键可任意类型,如对象 / 函数),替代传统对象;优势:键不限于字符串 / Symbol、可遍历、可获取长度(size)。

5. 模块化(ES Module)与 CommonJS 的区别?

难度:★★★标准答案

特性CommonJS(Node)ES Module(ES6)
加载时机运行时加载编译时静态分析
导出 / 导入module.exports/requireexport/import
拷贝 vs 引用值拷贝(导出后修改不影响导入)引用(实时关联)
默认值支持(module.exports = {}支持(export default
动态加载支持(require('./path' + name)需 import() 动态导入

6. Proxy 与 Object.defineProperty 的区别?(Vue2/Vue3 响应式原理)

难度:★★★★标准答案

特性Object.definePropertyProxy
监听范围仅对象属性(需遍历)整个对象(无需遍历)
支持的操作仅 get/setget/set/deleteProperty/has 等 13 种操作
数组监听需重写数组方法(如 push)原生支持数组监听
新增属性需手动监听自动监听
性能较差(遍历属性)更优
兼容性IE9+ES6+(无 IE 支持)

核心考点:Vue2 用 Object.defineProperty,Vue3 用 Proxy 重构响应式。

7. Symbol 的用途?

难度:★★☆标准答案

  • 生成唯一标识符(Symbol('a') !== Symbol('a'));

  • 用途:

    1. 对象唯一属性名(避免属性冲突);
    2. 定义常量(如 const TYPE = Symbol('type'));
    3. 实现迭代器(Symbol.iterator);
    4. 模拟私有属性(无法被 for...in 遍历)。

8. 可选链(?.)、空值合并运算符(??)的用途?

难度:★☆☆标准答案

  • 可选链 ?.:避免访问嵌套对象属性时的 Cannot read property 'x' of undefined 错误;示例:obj?.a?.b(obj/a 为 undefined 时返回 undefined);
  • 空值合并 ??:仅当值为 null/undefined 时返回默认值(区别于 |||| 会把 0/''/false 视为假值);示例:const num = 0 ?? 10 → 0const num = undefined ?? 10 → 10

五、DOM/BOM(初级 / 中级)

1. 事件冒泡、事件捕获、事件委托的原理?

难度:★★☆标准答案

  • 事件冒泡:事件从触发元素向上传播到父元素 / 根元素;
  • 事件捕获:事件从根元素向下传播到触发元素;
  • 事件流:捕获阶段 → 目标阶段 → 冒泡阶段;
  • 事件委托:利用事件冒泡,将子元素事件绑定到父元素,通过 event.target 判断触发元素;优势:减少事件绑定数量、支持动态新增子元素。

2. 如何阻止事件冒泡 / 默认行为?

难度:★☆☆标准答案

  • 阻止冒泡:event.stopPropagation()(阻止冒泡 / 捕获)、event.stopImmediatePropagation()(阻止后续同元素的事件处理);
  • 阻止默认行为:event.preventDefault()(如阻止链接跳转、表单提交)。

3. DOM 操作的优化方式?

难度:★★☆标准答案

  1. 减少 DOM 操作次数(批量操作,如文档碎片 document.createDocumentFragment);
  2. 避免频繁重排 / 重绘(如隐藏元素后修改样式、批量修改样式);
  3. 缓存 DOM 节点(避免重复 querySelector);
  4. 使用事件委托减少事件绑定;
  5. 用 requestAnimationFrame 优化动画(避免卡顿)。

4. 重排(回流)vs 重绘的区别?如何避免?

难度:★★☆标准答案

  • 重排:DOM 布局变化(如宽高、位置、节点增删),触发页面重新计算布局,性能消耗大;

  • 重绘:DOM 样式变化(如颜色、背景),不影响布局,性能消耗小;

  • 避免策略:

    1. 批量修改样式(class 替换 /cssText);
    2. 隐藏元素后修改(display: none);
    3. 使用 transform 做动画(GPU 加速,不触发重排);
    4. 避免频繁读取布局属性(如 offsetTop,会强制刷新布局)。

5. BOM 常用 API?

难度:★☆☆标准答案

  • 窗口操作:window.open/closewindow.resizeTo
  • 导航 / 历史:location.hrefhistory.pushState/replaceState
  • 定时器:setTimeout/setInterval/requestAnimationFrame
  • 存储:localStorage/sessionStorage/cookie
  • 其他:navigator.userAgent(判断浏览器)、screen.width(屏幕宽度)。

六、性能优化 & 安全(中级 / 高级)

1. 防抖(debounce)与节流(throttle)的区别?实现方式?

难度:★★★标准答案

  • 防抖:触发事件后延迟 n 秒执行,期间再次触发则重置延迟(适合搜索框输入、按钮防重复点击);手写:

javascript

运行

function debounce(fn, delay) {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
  • 节流:触发事件后,每隔 n 秒仅执行一次(适合滚动加载、窗口 resize);手写(时间戳版):

javascript

运行

function throttle(fn, delay) {
  let prev = 0;
  return (...args) => {
    const now = Date.now();
    if (now - prev >= delay) {
      fn.apply(this, args);
      prev = now;
    }
  };
}

2. JS 内存泄漏的原因?如何排查?

难度:★★★标准答案

  • 原因:

    1. 意外的全局变量(如未声明的变量、this 指向 window);
    2. 闭包未释放(如定时器引用闭包变量);
    3. DOM 引用未释放(如变量保存已删除的 DOM 节点);
    4. 定时器 / 事件监听未清除;
  • 排查工具:Chrome DevTools(Memory 面板)、Performance 面板;

  • 解决:手动释放引用(fn = null)、清除定时器 / 事件监听。

3. 前端性能优化的核心方向?(从加载→运行→渲染)

难度:★★★★标准答案

1. 加载优化
  • 资源压缩:JS/CSS/ 图片压缩、Gzip 传输;
  • 资源懒加载:图片 loading="lazy"、路由懒加载(import());
  • 缓存策略:强缓存(Cache-Control)、协商缓存(ETag/Last-Modified)、Service Worker 缓存;
  • 网络优化:CDN 加速、HTTP/2(多路复用)、预加载(preload/prefetch);
  • 减少请求:合并资源、雪碧图、内联小型资源。
2. 运行优化
  • 减少重排 / 重绘(见上文);
  • 防抖节流;
  • 避免长任务(拆分大函数,用 requestIdleCallback);
  • 优化循环(减少 DOM 操作、避免重复计算)。
3. 渲染优化
  • CSS 放头部,JS 放底部(或 defer/async);
  • 骨架屏 / 懒加载;
  • 用 transform/opacity 做动画(GPU 加速);
  • 避免同步脚本阻塞渲染。

4. XSS 攻击的原理?防护手段?

难度:★★★标准答案

  • 原理:攻击者注入恶意 JS 脚本,窃取用户 Cookie/Token、伪造操作;

  • 防护:

    1. 输入输出转义(过滤 <script>/onclick 等);
    2. 敏感 Cookie 加 HttpOnly 属性(禁止 JS 读取);
    3. 开启 CSP(内容安全策略,限制脚本执行源);
    4. 使用 Vue/React 等框架(自动转义插值)。

5. CSRF 攻击的原理?防护手段?

难度:★★★标准答案

  • 原理:攻击者诱导用户在已登录状态下,向目标服务器发送恶意请求(浏览器自动携带 Cookie);

  • 防护:

    1. Cookie 加 SameSite 属性(限制跨站携带);
    2. 服务器验证 CSRF Token(请求携带随机 Token,与 Cookie 比对);
    3. 验证请求头 Referer/Origin(确认请求来源合法)。

6. 如何实现前端权限控制?

难度:★★★标准答案

  1. 路由权限:路由守卫(如 Vue Router beforeEach),未登录 / 无权限跳转登录页;
  2. 按钮权限:自定义指令(如 v-permission),隐藏 / 禁用无权限按钮;
  3. 接口权限:请求拦截器携带 Token,后端验证权限;
  4. 数据权限:后端返回当前用户可访问的数据,前端渲染。

七、设计模式 & 工程化(高级)

1. 前端常用设计模式?(单例 / 观察者 / 工厂)

难度:★★★★标准答案

  • 单例模式:保证一个类仅创建一个实例(如 Vuex store、弹窗组件);手写:

javascript

运行

const Singleton = (function() {
  let instance;
  return class {
    constructor() {
      if (!instance) instance = this;
      return instance;
    }
  };
})();
  • 观察者模式(发布 - 订阅):一对多依赖,发布者触发事件,订阅者接收通知(如 addEventListener、Vue 事件总线);
  • 工厂模式:封装对象创建逻辑,根据参数返回不同实例(如 React 组件创建)。

2. 前端工程化的核心内容?

难度:★★★标准答案

  • 构建工具:Webpack/Vite/Rollup(打包、编译、优化);
  • 包管理:npm/yarn/pnpm(依赖管理);
  • 代码规范:ESLint(语法检查)、Prettier(格式化)、husky(Git 钩子);
  • 模块化:ES Module/CommonJS;
  • 自动化:CI/CD(持续集成 / 部署)、自动化测试(Jest/Cypress);
  • 规范化:语义化版本、Commitlint(提交规范)、CHANGELOG 生成。

3. 如何实现前端模块化打包?(Webpack 核心原理)

难度:★★★★标准答案

  • Webpack 核心:将所有资源视为模块,通过入口文件递归解析依赖,打包为一个 / 多个 bundle;

  • 核心流程:

    1. 入口解析:从 entry 开始,解析模块依赖;
    2. 模块转换:通过 loader 转换非 JS 资源(如 css-loader 处理 CSS);
    3. 代码分割:splitChunks 拆分公共代码 / 异步代码;
    4. 产物输出:plugin 优化产物(如 HtmlWebpackPlugin 生成 HTML)。

八、手写代码题(面试必考)

1. 手写 call/apply/bind

javascript

运行

// call
Function.prototype.myCall = function(context = window, ...args) {
  const fn = Symbol('fn');
  context[fn] = this;
  const res = context[fn](...args);
  delete context[fn];
  return res;
};

// bind(返回函数,支持柯里化)
Function.prototype.myBind = function(context, ...args1) {
  const fn = this;
  return function(...args2) {
    return fn.apply(context, [...args1, ...args2]);
  };
};

2. 手写深拷贝(支持循环引用 / 特殊对象)

javascript

运行

function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (map.has(obj)) return map.get(obj); // 处理循环引用
  const clone = obj instanceof Date ? new Date(obj) : 
                obj instanceof RegExp ? new RegExp(obj) :
                Array.isArray(obj) ? [] : {};
  map.set(obj, clone); // 缓存已拷贝对象
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], map);
    }
  }
  return clone;
}

3. 数组去重(基础类型 / 对象数组)

javascript

运行

// 基础类型
const unique = arr => [...new Set(arr)];

// 对象数组(按 id 去重)
const uniqueObj = (arr, key) => {
  const map = new Map();
  return arr.filter(item => !map.has(item[key]) && map.set(item[key], true));
};

4. 数组扁平化(拍平任意层级)

javascript

运行

// 递归
function flatten(arr) {
  let res = [];
  arr.forEach(item => {
    if (Array.isArray(item)) res = res.concat(flatten(item));
    else res.push(item);
  });
  return res;
}

// 简洁版
const flatten = arr => arr.flat(Infinity);

5. 实现异步并发控制(限制同时执行的 Promise 数量)

javascript

运行

async function limitRequest(promises, limit) {
  const res = [];
  const executing = [];
  for (const p of promises) {
    const pWrap = Promise.resolve(p).then(val => {
      res.push(val);
      // 移除已完成的 Promise
      const idx = executing.indexOf(pWrap);
      if (idx > -1) executing.splice(idx, 1);
      return val;
    });
    executing.push(pWrap);
    // 达到限制,等待一个完成
    if (executing.length >= limit) {
      await Promise.race(executing);
    }
  }
  // 等待剩余 Promise 完成
  await Promise.all(executing);
  return res;
}