第一篇:别再被 “异步” 搞懵了!用奶茶店逻辑学会 JS 异步编程,从此告别回调地狱
你有没有过这样的经历?写 JS 代码时,明明顺序排得整整齐齐,结果运行起来却 “不按常理出牌”—— 比如刚请求完数据,想立刻用却发现是 undefined,或者循环里的定时器全挤在最后一起执行。别慌,这不是你代码写错了,而是没搞懂 JS 里最核心的 “异步逻辑”。今天咱们就用奶茶店的日常操作当例子,把异步编程讲得明明白白,再教你怎么用 Promise 和 async/await 彻底摆脱回调地狱。

一、先搞懂:为什么 JS 会有 “异步” 这种东西?
首先得明确一个关键点:JS 是 “单线程” 语言。啥意思?就好比一家奶茶店只有一个做奶茶的师傅,同一时间只能做一杯奶茶。如果师傅非要做完一杯再接下一杯,那后面排队的人早就等疯了 —— 这就是 “同步” 的问题。
为了解决这个问题,奶茶店会搞个 “取餐号” 系统:你点单后拿个号,师傅先做你的奶茶,但在等奶茶煮好的间隙,他可以先接下一个人的订单。等你的奶茶做好了,广播喊号你再来取 —— 这就是 “异步” 的逻辑。JS 里的异步操作,比如定时器、网络请求、读取文件,本质上都是 “师傅在等食材熟的间隙,先处理其他活”。
举个最简单的例子:
console.log("1. 点一杯珍珠奶茶");
setTimeout(() => {
console.log("2. 奶茶做好了,取餐");
}, 2000);
console.log("3. 拿完取餐号,去玩手机");
运行结果会是 1→3→2,而不是 1→2→3。因为 setTimeout 就像 “等奶茶煮好”,JS 会先把这个任务放到 “异步队列” 里,继续执行后面的 “拿号玩手机”,等主线程的活干完了,再回头执行异步队列里的 “取奶茶”。
这里要记住两个核心概念:
调用栈:JS 执行代码的地方,就像师傅正在做的那杯奶茶,同一时间只有一个任务。
任务队列:异步任务排队的地方,就像取餐号的列表,等调用栈空了才会按顺序执行。
二、从 “回调函数” 到 “Promise”:异步代码的进化史
早期的 JS 异步编程全靠 “回调函数”,比如用 AJAX 请求数据:
// 早期AJAX代码,回调嵌套噩梦的开始
$.get("/user", function(userData) {
$.get("/user/" + userData.id + "/orders", function(ordersData) {
$.get("/orders/" + ordersData[0].id + "/details", function(detailData) {
console.log("终于拿到订单详情了", detailData);
}, function(err) {
console.log("订单详情请求失败", err);
});
}, function(err) {
console.log("订单列表请求失败", err);
});
}, function(err) {
console.log("用户信息请求失败", err);
});
这段代码看起来就像 “俄罗斯套娃”,如果再嵌套几层,代码缩进能歪到屏幕外面 —— 这就是传说中的 “回调地狱”。不仅写起来费劲,调试的时候找错也得一层一层扒,简直是开发者的噩梦。
为了解决这个问题,ES6 推出了 “Promise”,把嵌套的回调改成了 “链式调用”,就像奶茶店点单时 “加珍珠→加奶盖→少糖” 这样一步一步选,逻辑清晰多了。
咱们用 Promise 重写上面的 AJAX 请求:
// 先把AJAX封装成返回Promise的函数
function getRequest(url) {
return new Promise((resolve, reject) => {
$.get(url, function(data) {
resolve(data); // 请求成功,把数据传出去
}, function(err) {
reject(err); // 请求失败,把错误传出去
});
});
}
// 链式调用,再也没有嵌套
getRequest("/user")
.then(userData => {
return getRequest("/user/" + userData.id + "/orders"); // 返回下一个Promise
})
.then(ordersData => {
return getRequest("/orders/" + ordersData[0].id + "/details");
})
.then(detailData => {
console.log("拿到订单详情了", detailData);
})
.catch(err => {
console.log("某个步骤失败了", err); // 所有错误在这里统一处理
});
是不是清爽多了?Promise 就像一个 “承诺”:我现在去做异步任务,成功了就告诉你结果(resolve),失败了也告诉你原因(reject)。然后用.then () 处理成功的情况,用.catch () 处理所有失败的情况,不用再每个回调里都写错误处理了。
这里要注意 Promise 的三个状态:
pending(等待中):刚点完单,奶茶还在做。
fulfilled(已成功):奶茶做好了,调用 resolve。
rejected(已失败):原料没了做不了,调用 reject。
一旦状态从 pending 变成另外两种,就再也改不了了 —— 就像奶茶做好了不能再变回原料,没原料也不能突然变出奶茶。
三、终极方案:用 async/await 写 “同步风格” 的异步代码
虽然 Promise 解决了回调地狱,但链式调用写多了,还是得写一堆.then (),不够直观。ES2017 推出的 async/await,直接把异步代码写成了同步的样子,堪称 “异步编程的语法糖天花板”。
还是刚才的请求案例,用 async/await 改写:
// 封装getRequest的代码不变
function getRequest(url) {
return new Promise((resolve, reject) => {
$.get(url, resolve, reject); // 简化写法,$.get的成功回调直接传resolve
});
}
// 关键:用async声明函数,里面可以用await
async function getOrderDetail() {
try {
const userData = await getRequest("/user"); // 等这个Promise成功,再继续
const ordersData = await getRequest("/user/" + userData.id + "/orders");
const detailData = await getRequest("/orders/" + ordersData[0].id + "/details");
console.log("拿到订单详情了", detailData);
} catch (err) {
console.log("某个步骤失败了", err); // 所有错误用try/catch捕获
}
}
getOrderDetail();
这段代码看起来就像 “先拿用户数据→再拿订单列表→最后拿订单详情” 的同步逻辑,但实际上每一步都是异步的,不会阻塞主线程。这就是 async/await 的魔力:用同步的写法,实现异步的功能。
不过用的时候要记住两个规则:
await 只能在 async 函数里用:就像只有在奶茶店(async 函数)里才能用取餐号(await),你在大街上用人家不认。
错误要用 try/catch 处理:Promise 的 reject 会变成 await 的错误,必须用 try/catch 捕获,不然会报未处理的错误。
四、实战避坑:异步编程里那些 “反常识” 的坑
学会了基础用法,咱们再聊聊实际开发中容易踩的坑,帮你少走弯路。
坑 1:forEach 里用 await,结果 “并行执行” 变 “串行执行”?
比如你想遍历一个数组,每个元素都发一个异步请求:
async function fetchAllData(arr) {
arr.forEach(async item => {
const data = await getRequest(`/data/${item}`);
console.log(data);
});
console.log("所有请求完成");
}
fetchAllData([1,2,3]);
你以为会是 “请求 1→完成 1→请求 2→完成 2→请求 3→完成 3→所有请求完成”,但实际结果是 “同时发 3 个请求→随机顺序打印 data→先打印‘所有请求完成’”。
为什么?因为 forEach 会立刻执行所有回调函数,每个 async 回调里的 await 都是独立的,不会阻塞 forEach 的循环。就像师傅同时接了 3 个订单,三个奶茶一起做,谁先好谁先出。
如果想让请求 “串行执行”(一个做完再做下一个),应该用 for…of 循环:
async function fetchAllData(arr) {
for (const item of arr) {
const data = await getRequest(`/data/${item}`);
console.log(data);
}
console.log("所有请求完成"); // 现在会等所有请求做完再打印
}
如果想 “并行执行”(同时做,效率更高),但要等所有请求都完成再处理,用 Promise.all ():
async function fetchAllData(arr) {
// 先把所有请求变成Promise数组
const promiseArr = arr.map(item => getRequest(`/data/${item}`));
// 等所有Promise都成功,才会返回结果数组
const allData = await Promise.all(promiseArr);
console.log(allData); // 按数组顺序返回结果,不管谁先完成
console.log("所有请求完成");
}
坑 2:Promise.resolve () 和 new Promise (resolve) 的区别?
有时候会看到两种写法:
// 写法1
const p1 = Promise.resolve("hello");
// 写法2
const p2 = new Promise(resolve => resolve("hello"));
这两种写法看起来一样,结果也一样,但在处理 “thenable 对象”(有 then 方法的对象)时会有区别。比如:
```bash
const thenable = {
then(resolve) {
setTimeout(() => resolve("延迟1秒"), 1000);
}
};
// 写法1:会等待thenable的then方法执行完
Promise.resolve(thenable).then(data => console.log(data)); // 1秒后打印
// 写法2:不会等待,直接把thenable作为结果返回
new Promise(resolve => resolve(thenable)).then(data => console.log(data)); // 立刻打印thenable对象
简单说,Promise.resolve () 会 “穿透” thenable 对象,等待它的异步操作完成;而 new Promise (resolve) 只会直接把 thenable 当成普通值返回。日常开发中,用 Promise.resolve () 更安全,除非有特殊需求。
坑 3:async 函数永远返回 Promise,哪怕你 return 一个普通值
比如:
async function add(a, b) {
return a + b; // 看似返回数字
}
const result = add(1, 2);
console.log(result); // 输出Promise { 3 },而不是3
所以如果想拿到 async 函数的返回值,必须用 await 或者.then ():
add(1,2).then(res => console.log(res)); // 3
// 或者在另一个async函数里用await
async function getResult() {
const res = await add(1,2);
console.log(res); // 3
}
五、总结:异步编程的 “学习路径”
看到这里,你应该对 JS 异步编程有了清晰的认识。最后给你总结一个学习路径,帮你巩固知识:
理解基础:先搞懂单线程、调用栈、任务队列的概念,知道异步任务为什么会 “插队”。
掌握 Promise:熟悉 Promise 的三种状态、resolve/reject 的用法,以及.then ()/.catch () 的链式调用。
用好 async/await:在 Promise 的基础上,学会用 async/await 简化代码,记住 try/catch 处理错误。
实战避坑:搞清楚 forEach 和 for…of 的区别、Promise.all () 的用法、async 函数的返回值特性,避免踩常见的坑。
其实异步编程并不难,关键是把 “奶茶店逻辑” 和代码对应起来,多写多练。下次再遇到异步相关的 bug,先想想 “师傅现在在做什么?哪些任务在排队?”,思路就会清晰很多。
第二篇:JS 数组方法从入门到精通:10 个常用方法让你少写 80% 循环,菜鸟也能变大神
数组是 JS 里最常用的数据结构,没有之一。不管是处理列表数据、渲染页面,还是做数据筛选,都离不开数组。但很多新手还在用 for 循环一遍一遍遍历数组,代码又长又容易出错。其实 JS 数组自带了几十种方法,用好这些方法能让你的代码简洁又高效,还能少写很多 bug。今天咱们就挑 10 个最常用的数组方法,从基础用法到实战技巧,用大白话讲明白,让你看完就能上手用。
一、先搞懂:数组方法的 “共性” 和 “区别”
在学具体方法之前,先明确几个重要概念,帮你避免混淆:
是否改变原数组:
「改变原数组」的方法( mutable ):比如 push、pop、splice,调用后原数组会被修改。
「不改变原数组」的方法( immutable ):比如 map、filter、slice,调用后会返回一个新数组,原数组不变。
开发中尽量多用不改变原数组的方法,避免不小心修改了原始数据,导致后续 bug。
是否有返回值:
有的方法返回新数组(如 map、filter),有的返回新元素(如 pop、shift),有的返回布尔值(如 every、some),有的没有返回值(如 forEach)。
别以为 “没返回值” 就没用,比如 forEach 就是专门用来遍历数组执行操作的。
回调函数的参数:
大部分数组方法(如 map、filter、forEach)都需要传一个回调函数,回调函数通常有三个参数:
[1,2,3].forEach((item, index, arr) => {
console.log("当前元素:", item);
console.log("当前索引:", index);
console.log("原数组:", arr);
});
currentValue:当前正在处理的数组元素(必填)。
index:当前元素的索引(可选)。
array:调用方法的原数组(可选)。
比如 forEach 的回调:
接下来,咱们逐个讲解 10 个常用方法,每个方法都包含 “基础用法 + 实战场景 + 注意事项”,确保你学完就能用。
二、10 个常用数组方法详解(附实战案例)
1. forEach:遍历数组的 “万能工具”
作用:遍历数组,对每个元素执行回调函数,没有返回值(返回 undefined)。
是否改变原数组:不改变,但可以在回调里手动修改原数组(不推荐)。
基础用法:
const fruits = ["苹果", "香蕉", "橙子"];
// 遍历数组,打印每个水果
fruits.forEach(fruit => {
console.log(fruit); // 依次输出:苹果、香蕉、橙子
});
// 带索引的写法
fruits.forEach((fruit, index) => {
console.log(`第${index+1}个水果:${fruit}`); // 第1个水果:苹果...
});
实战场景:渲染列表数据到页面。
比如后端返回一个商品列表,需要渲染到页面的 ul 里:
const products = [
{ id: 1, name: "手机", price: 3999 },
{ id: 2, name: "电脑", price: 5999 },
{ id: 3, name: "平板", price: 2999 }
];
const ul = document.querySelector("#productList");
// 遍历商品数组,创建li元素并添加到ul
products.forEach(product => {
const li = document.createElement("li");
li.innerHTML = `
<span>商品名:${product.name}</span>
<span>价格:${product.price}元</span>
`;
ul.appendChild(li);
});
注意事项:
forEach 不能中断循环(比如用 break 或 return 没用),如果需要中断循环,应该用 for 循环或 some 方法。
空数组不会执行回调函数,所以不用担心遍历到 undefined。