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

相关推荐
上单带刀不带妹1 小时前
【Axios 实战】网络图片地址转 File 对象,附跨域解决方案
开发语言·前端·javascript·vue
SuperEugene2 小时前
前端模块化与 import/export入门:从「乱成一团」到「清晰可维护」
前端·javascript·面试·vue
程序员林北北2 小时前
【前端进阶之旅】Vue3 + Three.js 实战:从零构建交互式 3D 立方体场景
前端·javascript·vue.js·react.js·3d·typescript
岱宗夫up2 小时前
【前端基础】HTML + CSS + JavaScript 基础(二)
开发语言·前端·javascript·css·架构·前端框架·html
我是苏苏2 小时前
Web开发:使用Ocelot+Nacos+WebApi作简单网关鉴权
前端·javascript·ui
SuperEugene2 小时前
Day.js API 不包含插件API的速查表
前端·javascript·面试
前端 贾公子2 小时前
Vue3 组件库的设计和实现原理(上)
javascript·vue.js·ecmascript
明月_清风3 小时前
浏览器时间管理大师:深度拆解 5 大核心调度 API
前端·javascript
明月_清风3 小时前
你不知道的 JS——现代系统级 API 篇
前端·javascript