从 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 内存模型的学习,你也能理解对象为什么会“拷贝后互相影响”,并知道何时需要深拷贝。