🔥 面试官:手写 Promise 封装 AJAX?这 5 个考点 90% 的人跪了!
前言 :在 AI 编程时代,手写代码能力还是前端工程师的"护城河"吗?本文从一道真实面试题出发,带你彻底搞懂 AJAX、Fetch、Promise、深拷贝、内存管理 五大核心考点。文末附完整代码模板,建议收藏!
📋 目录
markdown
1. 面试题引入:从 GitHub API 调用说起
2. AJAX vs Fetch:本质区别一张图看懂
3. 手写 getJSON:Promise 封装 AJAX 完整实现
4. 手写 sleep 函数:Promise 异步控制精髓
5. 深拷贝 vs 浅拷贝:内存模型终极解析
6. 面试高频考点总结 + 代码模板
1️⃣ 面试题引入:从 GitHub API 调用说起
先看一段"有问题"的代码,你能找出几个 bug?
html
<script>
const members = document.querySelector('.member');
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.github.com/orgs/lemoncode/members", true);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.status === 200 && xhr.readyState === 4){
const data = JSON.parse(xhr.responseText);
console.log(data);
}
}
// ❌ 致命错误:在回调外访问异步数据
const data = JSON.parse(xhr.responseText);
members.innerHTML = data.map(item => `<li>${item.login}</li>`).join("");
</script>
错误分析:
| 序号 | 问题 | 严重性 |
|---|---|---|
| 1 | 异步时序错误:回调外访问 responseText |
🔴 致命 |
| 2 | 选择器不匹配:.member vs members |
🔴 致命 |
| 3 | 缺少错误处理 | 🟡 中等 |
| 4 | 回调地狱风险 | 🟡 中等 |
2️⃣ AJAX vs Fetch:本质区别一张图看懂
javascript
┌─────────────────────────────────────────────────────────────┐
│ AJAX vs Fetch 对比 │
├──────────────────┬──────────────────┬───────────────────────┤
│ 特性 │ AJAX │ Fetch │
├──────────────────┼──────────────────┼───────────────────────┤
│ 基于 │ XMLHttpRequest │ Promise │
│ 语法风格 │ 回调函数 │ then/catch/async-await│
│ 代码复杂度 │ 高 │ 低 │
│ 错误处理 │ onerror 监听 │ catch 捕获 │
│ 默认携带 cookie │ 是 │ 否 (需 credentials) │
│ 请求中止 │ abort() │ AbortController │
│ 浏览器兼容性 │ IE8+ │ IE 不支持 │
└──────────────────┴──────────────────┴───────────────────────┘
代码对比
javascript
// ❌ AJAX 写法(回调风格)
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
// 嵌套回调...
} else {
// 错误处理
}
}
};
// ✅ Fetch 写法(Promise 风格)
fetch(url)
.then(res => res.json())
.then(data => {
// 处理数据
})
.catch(err => {
// 错误处理
});
3️⃣ 手写 getJSON:Promise 封装 AJAX 完整实现
面试考点
面试官:请用 Promise 封装一个 getJSON 函数,支持 GET 请求,返回 JSON 数据。
完整实现
javascript
/**
* 手写 getJSON 函数 - Promise 封装 AJAX
* @param {string} url - 请求地址
* @returns {Promise} - 返回 Promise 实例
*/
const getJSON = (url) => {
return new Promise((resolve, reject) => {
// 1. 创建 XMLHttpRequest 实例
const xhr = new XMLHttpRequest();
// 2. 初始化请求(GET 方法,异步)
xhr.open('GET', url, true);
// 3. 设置请求头(可选)
xhr.setRequestHeader('Content-Type', 'application/json');
// 4. 发送请求
xhr.send();
// 5. 监听状态变化
xhr.onreadystatechange = function() {
// readyState 4 = 请求完成
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
// 解析 JSON 数据
const data = JSON.parse(xhr.responseText);
// 成功:调用 resolve
resolve(data);
} catch (e) {
// JSON 解析失败
reject(new Error('JSON 解析失败: ' + e.message));
}
} else {
// HTTP 状态码错误
reject(new Error(`请求失败,状态码: ${xhr.status}`));
}
}
};
// 6. 监听网络错误
xhr.onerror = function() {
reject(new Error('网络错误'));
};
// 7. 监听超时(可选)
xhr.ontimeout = function() {
reject(new Error('请求超时'));
};
});
};
使用示例
javascript
// 链式调用
getJSON('https://api.github.com/orgs/lemoncode/members')
.then(data => {
console.log('成员列表:', data);
return data[0]; // 返回第一个成员
})
.then(firstMember => {
console.log('第一个成员:', firstMember.login);
})
.catch(err => {
console.error('错误:', err.message);
})
.finally(() => {
console.log('请求完成(无论成功失败)');
});
// async/await 写法(更优雅)
async function fetchMembers() {
try {
const data = await getJSON('https://api.github.com/orgs/lemoncode/members');
console.log(data);
} catch (err) {
console.error(err);
} finally {
console.log('完成');
}
}
Promise 状态流转图
scss
┌─────────────┐
│ PENDING │
│ (等待中) │
└──────┬──────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐
│ FULFILLED │ │ REJECTED │
│ (成功) │ │ (失败) │
│ then 执行 │ │ catch 执行 │
└─────────────┘ └─────────────┘
4️⃣ 手写 sleep 函数:Promise 异步控制精髓
面试考点
面试官:如何用 Promise 实现一个 sleep 函数,让代码"暂停"指定时间?
完整实现
javascript
/**
* 手写 sleep 函数
* @param {number} ms - 休眠毫秒数
* @returns {Promise} - 返回 Promise 实例
*/
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 时间到,状态从 pending 变为 fulfilled
resolve(`休眠 ${ms}ms 完成`);
}, ms);
});
}
使用示例
javascript
// 链式调用
sleep(1000)
.then(msg => {
console.log(msg); // 1 秒后输出
return sleep(2000);
})
.then(msg => {
console.log(msg); // 再 2 秒后输出
});
// async/await 写法(推荐)
async function run() {
console.log('开始');
await sleep(1000);
console.log('1 秒后');
await sleep(2000);
console.log('3 秒后');
}
run();
⚠️ 常见错误
javascript
// ❌ 错误:在 setTimeout 中调用 reject 但没有意义
function sleep(n) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(); // 这会让 Promise 变为失败状态
}, n);
});
}
// 结果:会触发 catch,而不是 then
// ✅ 正确:应该调用 resolve
function sleep(n) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(); // 成功状态
}, n);
});
}
5️⃣ 深拷贝 vs 浅拷贝:内存模型终极解析
JS 内存模型
typescript
┌─────────────────────────────────────────────────────────────┐
│ JS 内存分布 │
├─────────────────────────┬───────────────────────────────────┤
│ 栈内存 (Stack) │ 堆内存 (Heap) │
├─────────────────────────┼───────────────────────────────────┤
│ • 简单数据类型 │ • 对象、数组、函数 │
│ (number, string, │ • 不连续存储 │
│ boolean, null, │ • 通过引用地址访问 │
│ undefined, symbol) │ • 垃圾回收机制管理 │
│ • 连续存储,访问快 │ • 存储实际数据 │
│ • 自动分配释放 │ │
└─────────────────────────┴───────────────────────────────────┘
浅拷贝 vs 深拷贝
javascript
const arr = [1, 2, { c: 3 }];
// 方法1:concat (浅拷贝)
const arr1 = arr.concat([]);
arr1[2].c = 999;
console.log(arr[2].c); // 999 ❌ 原数组被影响
// 方法2:JSON (深拷贝,有局限)
const arr2 = JSON.parse(JSON.stringify(arr));
arr2[2].c = 888;
console.log(arr[2].c); // 999 ✅ 原数组不受影响
// 方法3:structuredClone (现代浏览器)
const arr3 = structuredClone(arr);
// 方法4:递归实现 (完整深拷贝)
function deepClone(target, hash = new WeakMap()) {
if (target === null || typeof target !== 'object') return target;
if (hash.has(target)) return hash.get(target);
const clone = Array.isArray(target) ? [] : {};
hash.set(target, clone);
for (let key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], hash);
}
}
return clone;
}
方法对比表
| 方法 | 循环引用 | Date | RegExp | Function | 兼容性 |
|---|---|---|---|---|---|
concat/展开 |
❌ | ❌ | ❌ | ❌ | ✅ |
JSON |
❌ | ⚠️ | ⚠️ | ❌ | ✅ |
structuredClone |
✅ | ✅ | ✅ | ❌ | ⚠️ |
| 递归实现 | ✅ | ✅ | ✅ | ❌ | ✅ |
lodash.cloneDeep |
✅ | ✅ | ✅ | ❌ | ✅ |
6️⃣ 面试高频考点总结 + 代码模板
📝 核心考点清单
javascript
┌─────────────────────────────────────────────────────────────┐
│ 前端异步编程面试考点 │
├─────────────────────────────────────────────────────────────┤
│ 1. AJAX 原理:readyState、status、回调机制 │
│ 2. Fetch API:Promise 基础、链式调用、错误处理 │
│ 3. Promise 手写:状态流转、resolve/reject、then/catch │
│ 4. async/await:语法糖、错误处理、并行/串行执行 │
│ 5. 深拷贝实现:递归、循环引用、特殊类型处理 │
│ 6. 内存模型:栈/堆区别、引用传递、垃圾回收 │
└─────────────────────────────────────────────────────────────┘
🎯 万能代码模板
javascript
// ============ 模板1:Promise 封装 AJAX ============
function getJSON(url, options = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', url, true);
xhr.timeout = options.timeout || 5000;
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
reject(e);
}
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.ontimeout = () => reject(new Error('请求超时'));
xhr.send();
});
}
// ============ 模板2:sleep 函数 ============
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ============ 模板3:深拷贝函数 ============
function deepClone(target, hash = new WeakMap()) {
if (target === null || typeof target !== 'object') return target;
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target);
if (hash.has(target)) return hash.get(target);
const clone = Array.isArray(target) ? [] : {};
hash.set(target, clone);
for (let key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], hash);
}
}
return clone;
}
📚 延伸学习
| 主题 | 推荐文章 |
|---|---|
| Promise/A+ 规范 | Promises/A+ 官方规范 |
| Event Loop | JavaScript 事件循环详解 |
| 手写 Axios | 从零手写 Axios |
| 异步编程演进 | 从回调到 async/await |
💬 互动话题
你在面试中遇到过哪些"手写代码"的坑?
欢迎在评论区分享你的经历,点赞最高的送 前端面试题库 PDF 一份!
觉得有用?请点赞 + 收藏 + 关注,下期预告:《手写 Promise:从 0 实现 Promises/A+ 规范》
本文参考了稀土掘金多篇高赞文章,结合 2025-2026 年最新面试趋势整理而成。如有错误,欢迎指正!