🔥 面试官:手写 Promise 封装 AJAX?这 5 个考点 90% 的人跪了!

🔥 面试官:手写 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 年最新面试趋势整理而成。如有错误,欢迎指正!

相关推荐
吃西瓜的年年14 分钟前
TypeScript
javascript·ubuntu·typescript
熊猫_豆豆3 小时前
一个模拟四轴飞行器在随机气流扰动下悬停飞行的交互式3D仿真网页,包含飞行器建模与PID控制算法
javascript·3d·html·四轴无人机模拟飞行
来恩10034 小时前
jQuery选择器
前端·javascript·jquery
前端繁华如梦4 小时前
树上挂苹果还是挂玻璃球?Three.js 程序化果实的完整实现指南
前端·javascript
CDwenhuohuo5 小时前
优惠券组件直接用 uview plus
前端·javascript·vue.js
川冰ICE5 小时前
TypeScript装饰器与元编程实战
前端·javascript·typescript
AI砖家5 小时前
Vue3组件传参大全,各种传参方式的对比
前端·javascript·vue.js
希望永不加班5 小时前
var局部变量类型推断的利弊
java·服务器·前端·javascript·html
threelab6 小时前
Three.js 3D 地图可视化 | 三维可视化 / AI 提示词
前端·javascript·人工智能·3d·着色器
失眠的咕噜7 小时前
PDA 安卓设备上传多张图片
android·前端·javascript