JavaScript 常用面试题目|全面解析高频考点与实战技巧
本文系统整理 javascript面试题 中最常被问及的 20+ 核心问题,覆盖基础语法、闭包、原型链、事件循环、ES6+ 新特性、异步编程、性能优化、框架底层原理等模块,结合真实企业面试案例、错误答案分析、标准解法与扩展延伸,助您高效突破技术关卡。
一、为什么 JavaScript 是前端面试的“核心战场”?
在前端技术栈中,javascript常用面试题目之所以占据绝对权重,是因为 JavaScript 不仅是浏览器的唯一脚本语言,更是现代全栈开发的重要支柱。无论是 Vue、React 还是 Node.js,其底层逻辑都深深植根于 JS 语言机制。
🔹 面试考察的三大层次
- 基础理解层:变量提升、作用域、this 指向、类型转换等,考察语言基本功
- 机制原理层:原型链、执行上下文、事件循环、垃圾回收,考察底层认知深度
- 工程应用层:模块化、异步方案、性能调优、调试技巧,考察工程化能力
例如:某大厂一面常问“请说说 javascript常用面试题目 中你认为最易被误解的三个知识点”,这并非考察死记硬背,而是考察你对语言本质的反思能力与教学能力——能否把复杂概念讲得清晰易懂,是判断工程师是否具备技术影响力的关键。
二、基础概念高频题(附深度解析)
⚡ 题目1:var / let / const 的核心区别是什么?
常见错误答案:「var 有变量提升,let 和 const 没有」——这不完整!
标准答案应包含以下四维对比:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域({ }) | 块级作用域({ }) |
| 变量提升 | 是(提升并初始化为 undefined) | 是(但存在“暂时性死区”TDZ) | 是(同样存在 TDZ) |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 初始化要求 | 可不初始化 | 可不初始化 | 必须初始化 |
▶️ 延伸考点:TDZ(暂时性死区)导致的错误示例:
console.log(a); // undefined(var)
console.log(b); // ReferenceError: Cannot access 'b' before initialization(let)
let b = 2;
⚠️ 误区提醒:const 声明的“常量”并非完全不可变!对象和数组的属性/元素仍可修改:
const obj = { name: 'Alice' };
obj.name = 'Bob'; // ✅ 允许
obj = {}; // ❌ TypeError: Assignment to constant variable.
⚡ 题目2:null 与 undefined 的区别?
常见混淆点:「null 是空对象指针,undefined 是未定义」——这是定义,不是区别!
正确理解应从三方面展开:
- 语义:null 表示“有意 absence of any object value”;undefined 表示“missing value”
- 类型转换:Number(null) → 0;Number(undefined) → NaN
- == 与 ===:null == undefined → true;null === undefined → false
▶️ 实战场景:API 返回空数据时,应返回 null 还是 undefined?
- 返回 null:表示“存在该字段,但值为空”(如:用户信息存在,但邮箱未填)
- 返回 undefined:表示“该字段不存在”(如:旧版 API 没有 phone 字段)
⚡ 题目3:== 与 === 的使用原则
“== 会进行类型转换,=== 不会”——这是现象,不是原则!
工程化建议:
- ✅ 优先使用 ===(避免隐式转换陷阱,如 [] == ![] → true)
- ⚠️ 特殊场景可用 ==:判断 null/undefined(`if (val == null)` 等价于 `val === null || val === undefined`)
- ❌ 禁用:`if (val == '')`、`if (val == 0)` 等模糊比较
▶️ 扩展:Symbol.toPrimitive 如何影响 == 比较?
const obj = {
valueOf() { return 1; },
toString() { return '2'; }
};
console.log(obj == 1); // true(优先调用 valueOf)
console.log(obj == '2'); // true(字符串比较时调用 toString)
三、闭包:80% 面试者说不清的本质问题
⚡ 题目4:闭包的定义、作用与风险
❌ 错误答案:"闭包是函数嵌套函数"
✅ 正确定义:闭包是函数记住其词法作用域的能力,即使在其词法作用域之外执行
▶️ 核心三要素:
- 函数可以访问其外层作用域的变量
- 外层函数已返回,但内层函数仍被引用
- 被引用的变量不会被垃圾回收
function createCounter() {
let count = 0; // 被闭包引用,不会释放
return function() {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
⚠️ 内存泄漏风险:若闭包引用了大型 DOM 或循环引用,需手动解除引用:
element.onclick = function() {
// 错误:引用了外部变量且未清理
bigData.push(this.dataset);
};
⚡ 题目5:经典闭包面试题解析
问:以下代码输出什么?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
常见错误:输出 0,1,2
✅ 正确答案:输出 3,3,3(因 var 的函数作用域 + 闭包引用同一变量)
▶️ 三种解决方案:
// 方案1:let 块级作用域
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 方案2:IIFE 创建新作用域
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// 方案3:bind 绑定参数
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(null, i), 100);
}
四、原型链:从 [[Prototype]] 到继承机制
⚡ 题目6:__proto__ 与 prototype 的关系
❌ 常见混淆:"__proto__ 是对象的属性,prototype 是函数的属性"
✅ 精准表述:
- __proto__:对象的内部属性 [[Prototype]] 的访问器(非标准但广泛支持)
- prototype:函数对象独有的属性,用于设置其实例的 [[Prototype]]
- 关键关系:`obj.__proto__ === Constructor.prototype`
function Person() {}
const p = new Person();
console.log(p.__proto__ === Person.prototype); // true
▶️ 图解原型链:
object → Person.prototype → Function.prototype → Object.prototype → null
⚠️ 陷阱:箭头函数无 prototype 属性!
⚡ 题目7:class 与 prototype 的等价性
问:`class Person {}` 与 `function Person() {}` 本质相同吗?
✅ 是!class 是语法糖,编译后等价于:
function Person() {} // constructor
Object.defineProperty(Person, 'prototype', {
writable: false,
configurable: false,
// ...其他 descriptor
});
▶️ 继承对比:
// 传统原型链继承
function Animal() {}
Animal.prototype.eat = function() {}
function Dog() {}
Dog.prototype = new Animal(); // ❌ 问题:实例属性被共享
// class 继承(ES6)
class Animal {
eat() {}
}
class Dog extends Animal {} // ✅ 使用 Object.setPrototypeOf
五、事件循环:宏任务与微任务的执行顺序
⚡ 题目8:事件循环机制详解
❌ 错误理解:"JS 是单线程,所以异步任务会排队"
✅ 正确模型:
- 调用栈(Call Stack):执行同步代码
- 任务队列(Task Queue):存放宏任务(setTimeout、I/O)
- 微任务队列(Microtask Queue):存放微任务(Promise.then、MutationObserver)
- 执行规则:栈空 → 执行所有微任务 → 渲染 → 取一个宏任务
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// 输出顺序:A → D → C → B
▶️ 延伸:宏任务 vs 微任务
| 类型 | 示例 | 触发时机 |
|---|---|---|
| 宏任务 | setTimeout, setInterval, setImmediate | 下一轮事件循环 |
| 微任务 | Promise.then, queueMicrotask, MutationObserver | 当前事件循环末尾 |
⚡ 题目9:async/await 的底层实现
问:`async function` 返回什么?
✅ 返回一个 Promise 对象!其内部是 Generator 的语法糖。
async function foo() {
return 1;
}
// 等价于:
function foo() {
return Promise.resolve(1);
}
▶️ 执行流程:
- 遇到 await → 暂停执行,将后续代码作为微任务注册
- await 的 Promise resolve 后 → 恢复执行
async function test() {
console.log(1);
await console.log(2);
console.log(3);
}
test();
// 输出:1 → 2 → 3(注意:await 后的 console.log(2) 是同步执行的!)
六、ES6+ 核心特性:从 let 到 Proxy
⚡ 解构赋值:不只是简化代码
常见误区:`const { a, b } = { a: 1, b: 2 }` 只是语法糖
✅ 深度价值:
- 默认值:`const { a=1, b=2 } = obj`(仅当值为 undefined 时生效)
- 重命名:`const { name: userName } = user`
- 嵌套解构:`const { user: { profile: { age } } } = data`
▶️ 面试陷阱:解构时的错误边界处理
const { name } = null; // TypeError: Cannot destructure property 'name' of 'null'
⚡ 展开运算符:对象浅拷贝与合并
问:`{ ...obj1, ...obj2 }` 与 `Object.assign(obj1, obj2)` 有何区别?
✅ 关键差异:
| 特性 | 展开运算符 | Object.assign |
|---|---|---|
| 可枚举性 | 只复制可枚举属性 | 只复制可枚举属性 |
| 原型链 | 不复制原型 | 不复制原型 |
| getter/setter | 调用 getter 并设置新值 | 调用 getter 并设置新值 |
| 语法位置 | 可插入任意位置 | 目标对象必须在首位 |
▶️ 实际应用:数组去重(ES6)
const arr = [1, 2, 2, 3];
const unique = [...new Set(arr)]; // [1,2,3]
⚡ Proxy:拦截与定制对象操作
问:Proxy 与 Object.defineProperty 的区别?
✅ 核心优势:
- 拦截能力:Proxy 可拦截 13 种操作(get/set/has/deleteProperty 等)
- 数组支持:Object.defineProperty 无法拦截数组索引操作
- 性能:Proxy 在属性多时性能更优(无需遍历所有属性)
const handler = {
get(target, key) {
return key in target ? target[key] : 'default';
}
};
const p = new Proxy({}, handler);
console.log(p.name); // 'default'
⚠️ 限制:不能拦截 Proxy 内部的 this 绑定!
七、性能优化:从内存泄漏到渲染瓶颈
⚡ 题目10:如何排查内存泄漏?
✅ 四步排查法:
- 工具定位:Chrome DevTools → Memory → Heap Snapshot
- 关键指标:关注 "Detached DOM Trees"
- 常见场景:
- 未清理的定时器(setInterval)
- 事件监听未移除
- 闭包引用大对象
- 全局变量累积
- 解决方案:使用 WeakMap/WeakSet 存储缓存数据
// 错误:全局数组累积
const cache = [];
function loadData() {
cache.push(fetchData()); // 内存持续增长
}
// 正确:WeakMap 自动清理
const cache = new WeakMap();
function loadData(data) {
cache.set(data, processData(data));
}
⚡ 题目11:减少重排与重绘
❌ 错误实践:频繁操作 DOM
// 禁止:多次触发重排
div.style.width = '100px';
div.style.height = '100px';
div.style.color = 'red';
✅ 优化方案:
- 批量操作:使用 documentFragment
- 离线操作:display:none → 修改 → display:block
- 样式分离:通过 className 替代 inline style
- 硬件加速:transform/opacity 替代 top/left
▶️ 进阶:requestAnimationFrame 与 throttle 结合
let pending = false;
function update() {
if (!pending) {
pending = true;
requestAnimationFrame(() => {
// 执行 DOM 更新
pending = false;
});
}
}
八、框架原理:从 React 到 Vue 的底层逻辑
⚡ 题目12:虚拟 DOM 的优缺点
❌ 误区:"虚拟 DOM 更快"
✅ 真实对比:
| 场景 | 虚拟 DOM | 直接 DOM 操作 |
|---|---|---|
| 首次渲染 | 慢(需创建 JS 对象) | 快 |
| 小规模更新 | 快(diff 算法) | 快(直接操作) |
| 大规模更新 | 快(批量更新) | 慢(多次重排) |
| 复杂交互 | 优势明显 | 代码复杂易出错 |
▶️ 关键机制:
- diff 算法:同层比较(O(n) 而非 O(n³))
- key 的作用:标识节点身份(避免移动导致重渲染)
- 静态节点提升:跳过不变节点的 diff
⚡ 题目13:响应式原理对比
Vue 2:Object.defineProperty
// 限制:无法监听数组索引与新增属性
const obj = {};
Object.defineProperty(obj, 'name', {
get() { return value; },
set(v) { update(v); }
});
Vue 3:Proxy
const obj = new Proxy({}, {
get(target, key) { return Reflect.get(...); },
set(target, key, value) { return Reflect.set(...); }
});
React:状态驱动 + 重渲染
- 函数组件:通过 useState 更新状态
- 类组件:通过 setState 触发 shouldComponentUpdate
- 关键机制:Fiber 架构实现可中断渲染
// useState 的本质:调度更新
const [count, setCount] = useState(0);
setCount(count + 1); // 触发调度 → 新渲染单元