JavaScript 全维度面试题(附标准答案 + 考点分析)
以下覆盖 JavaScript 面试所有核心模块,按「基础语法→进阶原理→异步编程→ES6+→DOM/BOM→性能 / 安全→工程化→手写代码」分类,适配初级 / 中级 / 高级面试,每个问题标注「难度」和「核心考点」,既适合快速复习,也能应对深度追问。
一、基础语法(初级必掌握)
1. JS 有哪些数据类型?区别是什么?
难度:★☆☆标准答案:
-
基础类型(7 种):
String、Number、Boolean、Null、Undefined、Symbol(ES6)、BigInt(ES10);- 特点:存储在栈内存,值不可变,赋值是 “值拷贝”;
-
引用类型:
Object(包含Array、Function、Date、RegExp、Set/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) → NaN、Number(null) → 0;undefined === null → false、undefined == null → true。
4. 闭包的定义、用途、优缺点?
难度:★★☆标准答案:
-
定义:函数嵌套时,内部函数引用外部函数的变量 / 参数,外部函数执行后,变量仍被内部函数保留(不被垃圾回收),形成闭包;
-
核心用途:
- 私有化变量(如模块化:避免全局污染);
- 缓存数据(如计算结果缓存);
- 延长变量生命周期(如防抖节流);
-
优点:模块化、数据私有;
-
缺点:变量长期驻留内存,易导致内存泄漏(需手动释放:
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 指向的完整规则?
难度:★★★标准答案(优先级从高到低):
- new 绑定:
new 构造函数→this指向新创建的实例; - 显式绑定:
call/apply/bind→this指向绑定的对象; - 隐式绑定:
obj.fn()→this指向obj; - 默认绑定:独立调用函数 → 全局环境(浏览器
window,Nodeglobal),严格模式下为undefined; - 箭头函数:无自身
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 操作符的执行过程?
难度:★★★标准答案:
- 创建一个空对象
obj; obj.__proto__ = 构造函数.prototype(绑定原型);- 执行构造函数,
this指向obj(传参); - 若构造函数返回引用类型,返回该值;否则返回
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 是单线程,通过事件循环实现异步,分为「调用栈」「宏任务队列」「微任务队列」;
-
执行顺序:
- 执行同步代码(调用栈);
- 执行完同步代码后,清空所有微任务(按顺序);
- 执行一个宏任务,再清空所有微任务;
- 重复步骤 3,直到队列清空;
-
任务类型:
- 宏任务:
script整体、setTimeout/setInterval、DOM 事件、AJAX、setImmediate(Node); - 微任务:
Promise.then/catch/finally、async/await、MutationObserver、process.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 个状态:
pending→fulfilled/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. 回调地狱的解决方式?
难度:★★☆标准答案(从差到优):
- 嵌套回调(地狱,不推荐);
- Promise 链式调用(
then串联); async/await(最优,同步化代码);- 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 的区别?
难度:★☆☆标准答案:
| 特性 | var | let/const |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 变量提升 | 有(可先使用后声明) | 有(但处于 “暂时性死区”,不可先使用) |
| 重复声明 | 允许 | 不允许 |
| 初始值 | 可选 | const 必须初始化 |
| 绑定全局对象 | 是(挂载到 window) | 否 |
2. 箭头函数与普通函数的区别?
难度:★★☆标准答案:
- 无自身
this,继承外层作用域的this(无法被call/apply/bind修改); - 无
arguments对象(需用剩余参数...args替代); - 不能作为构造函数(不能
new); - 无
prototype属性; - 语法更简洁,适合回调函数(如数组方法)。
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/require | export/import |
| 拷贝 vs 引用 | 值拷贝(导出后修改不影响导入) | 引用(实时关联) |
| 默认值 | 支持(module.exports = {}) | 支持(export default) |
| 动态加载 | 支持(require('./path' + name)) | 需 import() 动态导入 |
6. Proxy 与 Object.defineProperty 的区别?(Vue2/Vue3 响应式原理)
难度:★★★★标准答案:
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 监听范围 | 仅对象属性(需遍历) | 整个对象(无需遍历) |
| 支持的操作 | 仅 get/set | get/set/deleteProperty/has 等 13 种操作 |
| 数组监听 | 需重写数组方法(如 push) | 原生支持数组监听 |
| 新增属性 | 需手动监听 | 自动监听 |
| 性能 | 较差(遍历属性) | 更优 |
| 兼容性 | IE9+ | ES6+(无 IE 支持) |
核心考点:Vue2 用 Object.defineProperty,Vue3 用 Proxy 重构响应式。
7. Symbol 的用途?
难度:★★☆标准答案:
-
生成唯一标识符(
Symbol('a') !== Symbol('a')); -
用途:
- 对象唯一属性名(避免属性冲突);
- 定义常量(如
const TYPE = Symbol('type')); - 实现迭代器(
Symbol.iterator); - 模拟私有属性(无法被
for...in遍历)。
8. 可选链(?.)、空值合并运算符(??)的用途?
难度:★☆☆标准答案:
- 可选链
?.:避免访问嵌套对象属性时的Cannot read property 'x' of undefined错误;示例:obj?.a?.b(obj/a 为 undefined 时返回 undefined); - 空值合并
??:仅当值为null/undefined时返回默认值(区别于||,||会把 0/''/false 视为假值);示例:const num = 0 ?? 10 → 0,const num = undefined ?? 10 → 10。
五、DOM/BOM(初级 / 中级)
1. 事件冒泡、事件捕获、事件委托的原理?
难度:★★☆标准答案:
- 事件冒泡:事件从触发元素向上传播到父元素 / 根元素;
- 事件捕获:事件从根元素向下传播到触发元素;
- 事件流:捕获阶段 → 目标阶段 → 冒泡阶段;
- 事件委托:利用事件冒泡,将子元素事件绑定到父元素,通过
event.target判断触发元素;优势:减少事件绑定数量、支持动态新增子元素。
2. 如何阻止事件冒泡 / 默认行为?
难度:★☆☆标准答案:
- 阻止冒泡:
event.stopPropagation()(阻止冒泡 / 捕获)、event.stopImmediatePropagation()(阻止后续同元素的事件处理); - 阻止默认行为:
event.preventDefault()(如阻止链接跳转、表单提交)。
3. DOM 操作的优化方式?
难度:★★☆标准答案:
- 减少 DOM 操作次数(批量操作,如文档碎片
document.createDocumentFragment); - 避免频繁重排 / 重绘(如隐藏元素后修改样式、批量修改样式);
- 缓存 DOM 节点(避免重复
querySelector); - 使用事件委托减少事件绑定;
- 用
requestAnimationFrame优化动画(避免卡顿)。
4. 重排(回流)vs 重绘的区别?如何避免?
难度:★★☆标准答案:
-
重排:DOM 布局变化(如宽高、位置、节点增删),触发页面重新计算布局,性能消耗大;
-
重绘:DOM 样式变化(如颜色、背景),不影响布局,性能消耗小;
-
避免策略:
- 批量修改样式(
class替换 /cssText); - 隐藏元素后修改(
display: none); - 使用
transform做动画(GPU 加速,不触发重排); - 避免频繁读取布局属性(如
offsetTop,会强制刷新布局)。
- 批量修改样式(
5. BOM 常用 API?
难度:★☆☆标准答案:
- 窗口操作:
window.open/close、window.resizeTo; - 导航 / 历史:
location.href、history.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 内存泄漏的原因?如何排查?
难度:★★★标准答案:
-
原因:
- 意外的全局变量(如未声明的变量、
this指向 window); - 闭包未释放(如定时器引用闭包变量);
- DOM 引用未释放(如变量保存已删除的 DOM 节点);
- 定时器 / 事件监听未清除;
- 意外的全局变量(如未声明的变量、
-
排查工具: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、伪造操作;
-
防护:
- 输入输出转义(过滤
<script>/onclick等); - 敏感 Cookie 加
HttpOnly属性(禁止 JS 读取); - 开启 CSP(内容安全策略,限制脚本执行源);
- 使用 Vue/React 等框架(自动转义插值)。
- 输入输出转义(过滤
5. CSRF 攻击的原理?防护手段?
难度:★★★标准答案:
-
原理:攻击者诱导用户在已登录状态下,向目标服务器发送恶意请求(浏览器自动携带 Cookie);
-
防护:
- Cookie 加
SameSite属性(限制跨站携带); - 服务器验证 CSRF Token(请求携带随机 Token,与 Cookie 比对);
- 验证请求头
Referer/Origin(确认请求来源合法)。
- Cookie 加
6. 如何实现前端权限控制?
难度:★★★标准答案:
- 路由权限:路由守卫(如 Vue Router
beforeEach),未登录 / 无权限跳转登录页; - 按钮权限:自定义指令(如
v-permission),隐藏 / 禁用无权限按钮; - 接口权限:请求拦截器携带 Token,后端验证权限;
- 数据权限:后端返回当前用户可访问的数据,前端渲染。
七、设计模式 & 工程化(高级)
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;
-
核心流程:
- 入口解析:从
entry开始,解析模块依赖; - 模块转换:通过
loader转换非 JS 资源(如css-loader处理 CSS); - 代码分割:
splitChunks拆分公共代码 / 异步代码; - 产物输出:
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;
}