从 Ajax 到 Promise:深入理解 getJSON 封装、异步流程控制与引用式拷贝

36 阅读4分钟

从 Ajax 到 Promise:深入理解 getJSON 封装、异步流程控制与引用式拷贝

在前端开发中,处理异步请求几乎是每天都会遇到的任务。随着 JavaScript 的不断发展,异步代码的写法也从最早的 Ajax 回调,逐渐演进到 Promise 与 Fetch。


一、Ajax 与 Fetch:异步通信方式的升级

最早在浏览器中发送请求依赖的是 XMLHttpRequest(XHR),也就是我们常说的 Ajax。

随着 ES6 发展,新的 Fetch API 逐渐成为主流,两者的体验差异非常明显。

1. Fetch 的优势

Fetch 可以看作是“更现代的 Ajax”,它有几个明显的好处:

  • 基于 Promise 实现,写起来更清晰。
  • 支持 async/await,让异步看起来像同步流程。
  • 使用更简洁,不需要监听各种状态。
  • 内置 .json() 等数据解析方法。

示例对比一下就能看出差距:

Ajax:

xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) { ... }
}

Fetch:

fetch(url).then(res => res.json())

一眼就能看出fetch更友好。

2. Ajax 仍然有用的场景

虽然 Fetch 更现代,但理解 Ajax 仍然非常重要,尤其是在以下情况:

  • 手写请求封装(如本篇的 getJSON)
  • 兼容某些老旧系统
  • 深入理解浏览器底层网络机制

你可以把 Ajax 当成“地基”,而 Fetch 是“封装后更好用的工具”。


二、手写一个基于 Ajax 与 Promise 的 getJSON

为了更直观地理解 Promise 的价值,我们把 XHR 手动封装成一个 Promise 风格的函数。

代码如下:

const getJSON = (url) => {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", url, true);
        xhr.send();

        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    try {
                        const data = JSON.parse(xhr.responseText);
                        resolve(data);
                    } catch (err) {
                        reject(err);
                    }
                } else {
                    reject(new Error(`HTTP Error: ${xhr.status}`));
                }
            }
        };

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

简单示例:

getJSON("https:...")
    .then(data => console.log(data))
    .catch(err => console.error(err))
    .finally(() => console.log("finished"));

通过 Promise,我们把原本需要嵌套回调的 XHR 调用,改造得更加直观和好维护。


三、Promise:现代异步代码的基础

Promise 的出现,是为了解决异步代码结构混乱的问题。它让我们能够用“链式逻辑”来管理异步流程。

1. Promise 的三个状态

Promise 只有三种可能的状态:

  • pending:执行中
  • fulfilled:成功
  • rejected:失败

一旦进入成功或失败,就不能再改变,这是 Promise 的核心特性。

2. Promise 的基本组成

  • 构造函数接收一个 executor 执行器函数
  • executor 内部会收到两个方法:resolve()reject()
  • .then() 处理成功
  • .catch() 处理错误
  • .finally() 做收尾工作

示例:手写 sleep

const sleep = (n) =>
    new Promise(resolve => setTimeout(resolve, n));

sleep(2000).then(() => console.log("Wake up!"));

这个例子展示了 Promise 是如何优雅地控制异步流程的。


四、JavaScript 内存模型:引用式拷贝为什么会“连带修改”?

理解拷贝行为,必须先弄清楚 JS 的两种内存:

1. 栈内存(Stack)

存储基本类型的 ,例如:

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol
  • bigint

这些值小而简单,直接存在栈中。

2. 堆内存(Heap)

所有对象类型(Object、Array、Function)都存在堆中。

变量里保存的不是对象本身,而是 指向堆内存的地址

引用式拷贝示例

const arr = [1, 2, 3];
const copy = arr;

copy[0] = 100;

console.log(arr); // [100, 2, 3]

两个变量指向同一地址,所以修改一方另一方也会变化。

这就是“引用式拷贝”的核心。


五、深拷贝:JSON.stringify 的能力与限制

最简单也最常见的深拷贝方式是:

const arr = [1, 2, 3];
const arr2 = JSON.parse(JSON.stringify(arr));

虽然简单,但它有不少缺陷。

无法处理的数据类型

类型结果
function被丢弃
undefined被丢弃
symbol被丢弃
Date变成字符串
RegExp变成空对象
循环引用直接报错

因此,它只适合:

  • 数据结构简单
  • 不包含特殊类型

想要真正的深拷贝,需要自己写算法(如 DFS/BFS)或使用 lodash 的 cloneDeep


结语:建立扎实的异步和内存基础

通过对 Ajax、Fetch、Promise 的拆解,你可以更清楚地理解现代前端是如何处理异步逻辑的。
而通过 JS 内存模型的学习,你也能理解对象为什么会“拷贝后互相影响”,并知道何时需要深拷贝。