普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月15日首页

《大厂面试:从手写 Ajax 到封装 getJSON,再到理解 Promise 与 sleep》

作者 xhxxx
2025年11月14日 23:03

大厂面试必考:从手写 Ajax 到封装 getJSON,再到理解 Promise 与 sleep

在前端工程师的求职过程中,尤其是冲击一线大厂(如阿里、腾讯、字节等)时,手写代码题几乎是绕不开的一环。这些题目看似基础,实则考察候选人对 JavaScript 核心机制的理解深度——包括异步编程、事件循环、内存模型以及浏览器原生 API 的掌握程度。

本文将焦三个经典手写题:

  1. 手写原生 Ajax
  2. 封装支持 Promise 的 getJSON 函数
  3. 手写 sleep 函数

我们将逐层深入,不仅写出代码,更要讲清楚“为什么这么写”,帮助你在面试中不仅能写出来,还能讲明白。


一、手写 Ajax:回调地狱的起点

虽然现代开发中我们早已习惯使用 fetchaxios,但 Ajax 是所有网络请求的基石。面试官让你手写 Ajax,不是为了让你重复造轮子,而是检验你是否真正理解 HTTP 请求在浏览器中的实现方式。 ajax 基于回调函数实现,代码复杂,这正是其痛点所在。

手写一个基础版 Ajax

function ajax(url, callback) {
  const xhr = new XMLHttpRequest();
  
  xhr.open('GET', url, true); // 异步请求
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) { // 请求完成
      if (xhr.status >= 200 && xhr.status < 300) {
        // 成功:调用回调并传入响应数据
        callback(null, JSON.parse(xhr.responseText));
      } else {
        // 失败:传入错误
        callback(new Error(`HTTP ${xhr.status}`), null);
      }
    }
  };
  xhr.send();
}

问题在哪?

  • 强依赖回调函数:调用方必须传入 callback,无法链式操作;
  • 错误处理分散:成功和失败逻辑耦合在同一个函数里;
  • 无法组合多个异步操作:比如“先请求 A,再根据 A 的结果请求 B”,代码会迅速变得嵌套混乱——即所谓的“回调地狱”。

这正是为什么我们需要 Promise


二、封装 getJSON:用 Promise 改造 Ajax

“如何封装一个 getJSON 函数。使用 ajax,支持 Promise,get 请求方法,返回是 JSON”

这其实是一个典型的“将传统回调式 API 转为 Promise 化”的过程。

封装思路

  • 创建一个返回 Promise 的函数;
  • Promise 构造函数内部执行 Ajax;
  • 成功时调用 resolve(data),失败时调用 reject(error)
  • 自动解析 JSON 响应体。

实现代码

function getJSON(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.setRequestHeader('Accept', 'application/json');

    xhr.onload = function () {
      if (xhr.status >= 200 && xhr.status < 300) {
        try {
          const data = JSON.parse(xhr.responseText);
          resolve(data);
        } catch (e) {
          reject(new Error('Invalid JSON response'));
        }
      } else {
        reject(new Error(`Request failed with status ${xhr.status}`));
      }
    };

    xhr.onerror = function () {
      reject(new Error('Network error'));
    };

    xhr.send();
  });
}

使用方式(对比 fetch)

// 使用我们封装的 getJSON
getJSON('/api/user')
  .then(user => console.log(user))
  .catch(err => console.error('Failed:', err));

// 等价于 fetch 写法(但 fetch 不自动抛出 HTTP 错误)
fetch('/api/user')
  .then(res => {
    if (!res.ok) throw new Error('HTTP error');
    return res.json();
  })
  .then(user => console.log(user))
  .catch(err => console.error(err));

为什么 Promise 更好?

“fetch 简单易用,基于 Promise 实现,(then)无需回调函数”

Promise 的核心优势在于:

  • 状态机模型:初始为 pending,只能变为 fulfilled(通过 resolve)或 rejected(通过 reject),且状态不可逆;
  • 链式调用.then().catch() 形成清晰的流程控制;
  • 统一错误处理:任意环节出错,都会被最近的 .catch 捕获。

这使得异步代码更接近同步逻辑的阅读体验。


三、深入 Promise:不只是语法糖

“promise 类 ,为异步变同步而(流程控制)实例化,事实标准。接收一个函数,函数有两个参数,resolve reject,他们也是函数。”

  • new Promise(executor) 中的 executor 是一个立即执行的函数;
  • 它接收两个参数:resolvereject,都是由 Promise 内部提供的函数;
  • 调用 resolve(value) 会将 Promise 状态转为 fulfilled,并将 value 传递给下一个 .then
  • 调用 reject(reason) 则转为 rejected,触发 .catch

例如:

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    Math.random() > 0.5 ? resolve('ok') : reject('fail');
  }, 1000);
});

p.then(console.log).catch(console.error);

这种设计让开发者能主动控制异步结果的“成功”或“失败”路径,是构建可靠异步系统的基础。


四、手写 sleep:Promise

实现

 function sleep(n){
            let p;
                 p = new Promise ((resolve,reject)=>{
                
                setTimeout(()=>{
                // pending 等待
                console.log(p);
                //resolve();
                reject();
                // fulfilled  成功
                console.log(p);
                }
                    ,n);
            })
            return p;
        }
        sleep(3000)
        .then(()=>{
            console.log('3s后执行');

        })
        .catch(()=>{
            console.log('3s后执行失败');
        })
        // promise 状态改变 就会执行
        .finally(()=>{
            console.log('finally');
        })

手写题的本质是理解机制

大厂面试之所以反复考察这些“老掉牙”的手写题,是因为它们像一面镜子,照出你对 JavaScript 运行机制的理解深度:

  • Ajax → 浏览器网络 API + 回调模型;
  • Promise 封装 → 异步流程控制范式升级;
  • sleep → Promise 与定时器的创造性结合;

当你不仅能写出这些代码,还能清晰解释其背后的原理时,你就已经超越了大多数候选人。

记住:面试不是考你会不会用库,而是考你知不知道库为什么存在。

昨天以前首页

《JavaScript Promise 完全解析:executor、状态与链式调用》

作者 xhxxx
2025年11月10日 23:16

深入理解 JavaScript Promise:从构造到状态流转

在现代 JavaScript 开发中,Promise 已成为处理异步操作的标准工具。但很多人只是“会用”,却不理解它背后的运行机制。本文将带你从零开始,深入剖析 Promise构造方式、内部状态变化规则,以及 .then().catch() 是如何协同工作的。


一、Promise 是如何构造的?

Promise 是一个内置的构造函数,通过 new Promise() 创建实例:

const myPromise = new Promise((resolve, reject) => {
  // executor(执行器)函数
});

构造函数接收一个参数:executor 函数

  • 这个函数会立即同步执行(不是异步!)。

  • Promise有三种状态

    • pending(待定)
    • fulfilled(已兑现)
    • rejected(已拒绝)
  • Promise两个内部属性

    • state
    • result
  • Promise接收两个参数:

    • resolve(value):用于将 Promise state变为 fulfilled(成功)、result:value

    • reject(error):用于将 Promise state变为 rejected(失败)、result:error

image.png

注意:即使你没有调用 resolvereject,Promise 也会一直处于 pending 状态,永远不会完成。

示例:基本构造

 console.log(1);
        //Promise 异步任务同步化
        //许诺 Promise 包含一个耗时性的任务
        const p=new Promise((resolve)=>{
            setTimeout(function() {
            console.log(2);
            resolve();
        }, 3000);
     
        })
        p.then(()=>{
            console.log(3);
        })
          console.log(4);

输出顺序:

image.png

1和4作为同步任务先执行
2在3秒后输出
当异步任务结束成功后.then处理函数输出3

这说明:executor 是同步执行的,但其中的异步逻辑(如 setTimeout)会被放入任务队列


二、Promise 状态的不可逆性

状态一旦改变,就不可逆转

const p = new Promise((resolve, reject) => {
  resolve('第一次 resolve');
  resolve('第二次 resolve'); // 无效!
  reject('尝试 reject');     // 也无效!
});

p.then(console.log); // 只输出 "第一次 resolve"

executor 只能调用一个 resolve 或一个 reject。任何状态的更改都是最终的。 所有其他的再对 resolve 和 reject 的调用都会被忽略:


三、.then():监听状态变化的桥梁

.then() 是 Promise 最核心的方法,用于注册成功和失败的回调函数

语法

promise.then(
  onFulfilled,   // 当状态变为 fulfilled 时调用
  onRejected     // 当状态变为 rejected 时调用(可选)
);

每次只根据异步处理结果运行一个函数

关键特性

  1. .then() 总是返回一个新的 Promise,支持链式调用。
  2. 如果 onFulfilled 返回一个值,新 Promise 会被 resolve 该值。
  3. 如果 onFulfilled 抛出异常,新 Promise 会被 reject

示例:成功与失败处理

const p = new Promise((resolve, reject) => {
  Math.random() > 0.5 ? resolve('OK') : reject(new Error('NO'));
});

p.then(
  result => console.log(' 成功:', result),
  error  => console.log(' 失败:', error.message)
);

提示:虽然 .then() 支持两个参数,但更推荐使用 .catch() 统一处理错误。


四、.catch():专为错误设计的处理器

.catch(onRejected).then(null, onRejected) 的语法糖。

promise
  .then(result => { /* 处理成功 */ })
  .catch(error => { /* 处理失败 */ });

为什么推荐用 .catch()

  • 统一错误捕获:链式调用中,任何一个环节抛出异常,都会被后续的 .catch() 捕获。
  • 避免遗漏错误处理:如果只用 .then() 的第二个参数,无法捕获 .then() 回调内部的错误。

对比示例

//  不推荐:无法捕获 then 内部的错误
promise.then(
  () => { throw new Error('Oops!'); },
  err => console.log('这里不会执行!')
);

//  推荐:能捕获所有前面的错误
promise
  .then(() => { throw new Error('Oops!'); })
  .catch(err => console.log('捕获到了:', err.message));

.catch其实是.then处理错误的一种简写方式.catch(f)调用是.then(null,f)的一种模拟


🔗 五、链式调用与状态传递

由于 .then().catch() 都返回新的 Promise,我们可以构建清晰的异步流程:

fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => render(posts))
  .catch(err => showError(err));

在这个链条中:

  • 每一步的返回值会作为下一步的输入;
  • 任何一步出错,都会跳转到最近的 .catch()


总结:Promise 的核心心智模型

  1. 构造即执行new Promise(executor) 会立即运行 executor。
  2. 状态单向不可逆pending → fulfilledpending → rejected,仅一次。
  3. .then() 是观察者:根据状态决定调用哪个回调。
  4. .catch() 是安全网:集中处理整个链路的异常。
  5. 链式调用靠返回新 Promise:实现异步流程的线性表达。

❌
❌