JavaScript 面试题精讲
面试考察的 JavaScript 知识有清晰的规律:手写题看基本功,原理题看理解深度,场景题看工程经验,设计题看架构能力。本文按这四类梳理高频题目,每题给出可直接运行的完整解答与关键讲解。
一、高频手写题
1.1 深拷贝
javascript
function deepClone(value, map = new WeakMap()) {
if (value === null || typeof value !== "object") return value;
if (map.has(value)) return map.get(value); // 解决循环引用
const result = Array.isArray(value) ? [] : {};
map.set(value, result);
for (const key of Reflect.ownKeys(value)) { // 含 Symbol 键
result[key] = deepClone(value[key], map);
}
return result;
}
const obj = { a: 1, b: [1, 2], c: { d: 3 } };
obj.self = obj; // 循环引用
const clone = deepClone(obj);
console.log(clone.a, clone.self === clone); // 1 true要点:递归 + WeakMap 缓存已克隆对象;Reflect.ownKeys 覆盖 Symbol 键;特殊类型(Date、RegExp)需按类型分支处理。
1.2 防抖与节流
javascript
// 防抖:停止触发 wait 后才执行(适合输入搜索)
function debounce(fn, wait = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
// 节流:固定间隔内只执行一次(适合滚动、拖拽)
function throttle(fn, interval = 300) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}| 对比 | 防抖 | 节流 |
|---|---|---|
| 行为 | 连续触发只执行最后一次 | 连续触发按固定频率执行 |
| 场景 | 搜索框、窗口 resize | 滚动监听、按钮防连点 |
1.3 Promise.all 与 Promise.race
javascript
Promise.myAll = function (promises) {
return new Promise((resolve, reject) => {
const results = [];
let count = 0;
promises.forEach((p, i) => {
Promise.resolve(p).then(
(value) => {
results[i] = value; // 保持原顺序
if (++count === promises.length) resolve(results);
},
reject // 一个失败即整体失败
);
});
});
};
Promise.myRace = function (promises) {
return new Promise((resolve, reject) => {
promises.forEach((p) => Promise.resolve(p).then(resolve, reject));
// 谁先落定就用谁的结果,其余忽略
});
};1.4 new 的实现
javascript
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype); // 1. 原型指向构造函数的 prototype
const result = Constructor.apply(obj, args); // 2. 执行构造函数,this 指向新对象
return (result !== null && typeof result === "object") || typeof result === "function"
? result // 3. 构造函数返回对象则用它
: obj; // 否则返回新对象
}
function Person(name) { this.name = name; }
const p = myNew(Person, "张三");
console.log(p instanceof Person, p.name); // true 张三1.5 call / apply / bind
javascript
Function.prototype.myCall = function (thisArg, ...args) {
const fn = Symbol("fn"); // 避免覆盖原属性
thisArg = thisArg ?? globalThis;
thisArg[fn] = this; // 把函数挂到 thisArg 上
const result = thisArg[fn](...args);
delete thisArg[fn];
return result;
};
Function.prototype.myBind = function (thisArg, ...bindArgs) {
const fn = this;
return function (...callArgs) {
return fn.myCall(thisArg, ...bindArgs, ...callArgs);
};
};
function greet(prefix) { return `${prefix},我是${this.name}`; }
const obj = { name: "李四" };
console.log(greet.myCall(obj, "你好")); // 你好,我是李四
console.log(greet.myBind(obj, "嗨")()); // 嗨,我是李四1.6 instanceof 与柯里化
javascript
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left);
const prototype = right.prototype;
while (proto) {
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto); // 沿原型链向上
}
return false;
}
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn(...args); // 参数够了直接执行
return (...more) => curried(...args, ...more); // 不够就继续收集
};
}
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 61.7 数组去重与扁平化
javascript
const unique = [...new Set([1, 1, 2, 3, 3])]; // Set 去重
const uniqueObj = arr.filter((v, i) => arr.indexOf(v) === i);
function flatten(arr, depth = Infinity) { // 支持指定深度
return arr.flat(depth);
}
function flattenRecursive(arr) {
return arr.reduce((acc, cur) =>
acc.concat(Array.isArray(cur) ? flattenRecursive(cur) : cur), []);
}
console.log(flatten([1, [2, [3, [4]]]], 2)); // [1, 2, 3, [4]]二、高频原理题
2.1 事件循环输出顺序
javascript
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
async function f() { await 1; console.log("4"); }
f();
// 输出:1 → 4 → 3 → 2讲解:await 后的代码相当于 .then 回调,与 Promise.resolve().then 按注册顺序执行(先注册先执行),都先于宏任务 setTimeout。
2.2 闭包应用:计数器与私有变量
javascript
function createCounter() {
let count = 0; // 外部无法直接访问
return {
increment: () => ++count,
get: () => count,
};
}
const c = createCounter();
c.increment(); c.increment();
console.log(c.get()); // 2
console.log(c.count); // undefined,闭包变量被"保护"2.3 this 指向五连问
javascript
const obj = {
name: "obj",
fn() { return this.name; },
arrow: () => this.name, // 箭头函数:定义时绑定,this 为外层
};
console.log(obj.fn()); // "obj",方法调用
const get = obj.fn;
console.log(get()); // undefined,独立调用 this 为全局/undefined
console.log(obj.fn.call({ name: "x" })); // "x",call 显式指定
console.log(new (function F() { this.name = "F"; })().name); // "F",new 绑定优先| 调用方式 | this |
|---|---|
| 普通函数调用 | 全局对象(严格模式为 undefined) |
方法调用 obj.fn() | 调用者 obj |
| call/apply/bind | 显式指定的对象 |
new 调用 | 新创建的对象 |
| 箭头函数 | 定义时外层作用域的 this |
2.4 原型链
javascript
function Animal() {}
Animal.prototype.run = function () { return "running"; };
function Dog() {}
Dog.prototype = Object.create(Animal.prototype); // 继承
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () { return "wang"; };
const dog = new Dog();
console.log(dog.run()); // running,沿原型链找到 Animal.prototype
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true,Dog.prototype 在 Animal 链上
console.log(dog instanceof Object); // true,原型链顶端是 Object.prototype2.5 宏微任务混合输出
javascript
setTimeout(() => console.log("A"), 0);
Promise.resolve().then(() => {
console.log("B");
setTimeout(() => console.log("C"), 0);
});
Promise.resolve().then(() => console.log("D"));
// 输出:B → D → A → C
// B、D 是微任务立即执行;A 先注册先执行;C 是 A 之后注册的下一个宏任务三、场景题
3.1 大文件上传
javascript
// 核心思路:分片 + 并发上传 + 进度汇总
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB 一片
function sliceFile(file) {
const chunks = [];
for (let start = 0; start < file.size; start += CHUNK_SIZE) {
chunks.push(file.slice(start, start + CHUNK_SIZE));
}
return chunks;
}
async function uploadInChunks(file, onProgress) {
const chunks = sliceFile(file);
let uploaded = 0;
await Promise.all(chunks.map(async (chunk, index) => {
const form = new FormData();
form.append("chunk", chunk);
form.append("index", index);
form.append("total", chunks.length);
await fetch("/api/upload/chunk", { method: "POST", body: form });
uploaded += chunk.size;
onProgress?.(Math.round((uploaded / file.size) * 100));
}));
// 通知后端合并分片
await fetch("/api/upload/merge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: file.name, total: chunks.length }),
});
}回答要点:切片(file.slice)、分片并发、失败分片重传、合并接口、断点续传(记录已传分片索引)。
3.2 并发请求控制
javascript
async function requestInLimit(urls, limit) {
const results = [];
let index = 0;
const worker = async () => {
while (index < urls.length) {
const i = index++;
try {
results[i] = await fetch(urls[i]).then((r) => r.json());
} catch (e) {
results[i] = { error: e.message };
}
}
};
await Promise.all(Array.from({ length: limit }, worker));
return results;
}
// 20 个接口,最多 4 个并发
const data = await requestInLimit(urls, 4);3.3 前端性能优化方案
| 维度 | 手段 |
|---|---|
| 加载 | 代码分割、按需加载、资源压缩、CDN、HTTP/2 |
| 渲染 | 避免重排重绘、requestAnimationFrame 批量更新、虚拟列表 |
| 计算 | 防抖节流、Web Worker 处理大数据、合理使用缓存 |
| 网络 | 请求合并、缓存策略、preload/prefetch |
| 感知 | 骨架屏、content-visibility、懒加载图片 |
javascript
// 示例:虚拟滚动只渲染可视区,避免千条 DOM 卡顿
function VirtualList({ items, itemHeight, viewportHeight }) {
const [start, setStart] = React.useState(0);
const count = Math.ceil(viewportHeight / itemHeight) + 2;
const visible = items.slice(start, start + count);
// 设置 translateY(start * itemHeight) 定位
}四、设计题
4.1 实现 EventEmitter
javascript
class EventEmitter {
#listeners = new Map();
on(event, fn) {
if (!this.#listeners.has(event)) this.#listeners.set(event, []);
this.#listeners.get(event).push(fn);
return this; // 支持链式调用
}
off(event, fn) {
const list = this.#listeners.get(event);
if (!list) return this;
this.#listeners.set(event, list.filter((f) => f !== fn));
return this;
}
emit(event, ...args) {
(this.#listeners.get(event) || []).forEach((fn) => fn(...args));
return this;
}
once(event, fn) {
const wrapper = (...args) => {
this.off(event, wrapper);
fn(...args);
};
return this.on(event, wrapper);
}
}
const bus = new EventEmitter();
const onHello = (msg) => console.log("收到:", msg);
bus.on("hello", onHello).emit("hello", "你好"); // 收到:你好
bus.off("hello", onHello).emit("hello", "无人接收"); // 无输出4.2 实现 LRU 缓存
javascript
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map(); // Map 保持插入顺序,队首最旧、队尾最新
}
get(key) {
if (!this.map.has(key)) return -1;
const value = this.map.get(key);
this.map.delete(key); // 先删除再插入 → 移到队尾(最近使用)
this.map.set(key, value);
return value;
}
put(key, value) {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, value);
if (this.map.size > this.capacity) {
const oldest = this.map.keys().next().value; // 队首最久未用
this.map.delete(oldest);
}
}
}
const cache = new LRUCache(2);
cache.put("a", 1); cache.put("b", 2);
cache.get("a"); // a 变为最近使用
cache.put("c", 3); // 超出容量,淘汰 b
console.log(cache.get("b")); // -1,已被淘汰4.3 发布订阅(Bus 模式)
javascript
function createPubSub() {
const topics = new Map();
return {
subscribe(topic, fn) {
if (!topics.has(topic)) topics.set(topic, new Set());
topics.get(topic).add(fn);
return () => topics.get(topic)?.delete(fn); // 返回退订函数
},
publish(topic, payload) {
topics.get(topic)?.forEach((fn) => fn(payload));
},
};
}
const { subscribe, publish } = createPubSub();
const unsub = subscribe("login", (user) => console.log("登录:", user.name));
publish("login", { name: "王五" }); // 登录:王五
unsub();
publish("login", { name: "赵六" }); // 已退订,无输出五、八股记忆要点
| 考点 | 核心结论 |
|---|---|
| 变量提升 | var 声明提升到作用域顶部但值为 undefined;let/const 存在暂时性死区;函数声明整体提升 |
| 类型转换 | 显式:Number()、String()、Boolean();隐式:+、==、比较运算符;null == undefined 为 true |
| 严格模式 | "use strict":禁止未声明赋值、禁止删除变量、this 在普通函数中为 undefined |
| 垃圾回收 | 可达性判定;新生代 Scavenger 复制回收、老年代 Mark-Compact;弱引用防泄漏 |
javascript
console.log(a); // undefined,var a 提升但未赋值
var a = 1;
// 严格模式示例
"use strict";
// b = 2; // ReferenceError: b is not defined
console.log([] == 0); // true,空数组转为 0
console.log([] == ![]); // true,经典隐式转换题掌握以上四类题型并理解背后的机制,面试中无论题目如何变形,都能从"原理 + 代码 + 工程实践"三个角度给出有深度的回答。