JavaScript 基础
1. 数组的一些方法
数组的基本操作
// 创建数组
const fruits = ["Apple", "Banana", "Cherry"];
// 访问元素
console.log(fruits[0]); // "Apple"
// 修改元素
fruits[1] = "Blueberry";
// 添加元素
fruits.push("Mango"); // 在末尾添加
fruits.unshift("Orange"); // 在开头添加
// 删除元素
fruits.pop(); // 删除最后一个
fruits.shift(); // 删除第一个
数组魔法方法
JavaScript 提供了一套功能强大的数组方法,用于以非常优雅的方式操作数据。这些方法基于函数式编程的思想,它们不会修改原数组,而是返回新的数组。
map 方法
map
方法期望一个回调函数作为参数。它会自动为我们遍历数组,对每个元素应用回调函数,并返回一个新数组。
function addOne(num) {
return num + 1;
}
const arr = [1, 2, 3, 4, 5];
const mappedArr = arr.map(addOne);
console.log(mappedArr); // 输出 [2, 3, 4, 5, 6]
// 原始数组没有被改变!
console.log(arr); // 输出 [1, 2, 3, 4, 5]
回调函数可以用箭头函数来简化书写:
const arr = [1, 2, 3, 4, 5];
const mappedArr = arr.map((num) => num + 1);
console.log(mappedArr); // 输出 [2, 3, 4, 5, 6]
filter 方法
filter
与 map
相似,但它不是转换数组中的值,而是返回一个新数组,新数组中只包含那些让回调函数返回 true
的项。
function isOdd(num) {
return num % 2 !== 0;
}
const arr = [1, 2, 3, 4, 5];
const oddNums = arr.filter(isOdd);
console.log(oddNums); // 输出 [1, 3, 5]
console.log(arr); // 输出 [1, 2, 3, 4, 5],原始数组不受影响
filter
将遍历 arr
并将每个值逐一传递给 isOdd
回调函数
reduce 方法
reduce
方法有两个关键区别:
-
回调函数接收两个参数:
- 累加器 (accumulator):循环中该点结果的当前值
- 当前值 (current value):当前正在迭代的项
-
reduce
本身还可以接收一个初始值 (initialValue) 作为可选的第二个参数
const arr = [1, 2, 3, 4, 5];
const productOfAllNums = arr.reduce((total, currentItem) => {
return total * currentItem;
}, 1);
console.log(productOfAllNums); // 输出 120
console.log(arr); // 输出 [1, 2, 3, 4, 5]
链式调用
可以通过数组方法链式调用的方式,让函数调用变得更加简洁,例如下面的 sumOfTripledEvens
函数实现:
function sumOfTripledEvens(array) {
return array
.filter((num) => num % 2 === 0) // 筛选出所有偶数
.map((num) => num * 3) // 将每个偶数乘以 3
.reduce((total, num) => total + num, 0); // 将它们全部相加
}
这种链式调用方式:
- 更具可读性:每一步的意图都很清晰
- 更少错误:函数式编程减少了状态变化
- 更易维护:每个步骤都是独立的、可测试的
2. DOM 操作与事件
文档对象模型 (DOM)
DOM (Document Object Model) 是浏览器为 HTML 文档创建的一个树状编程接口。它将文档中的所有内容(元素、文本、注释等)都表示为可编程的”节点 (nodes)“。这些节点根据它们在 HTML 中的层级关系,构成了类似家谱的树形结构。
<div id="container">
<div class="display"></div>
<div class="controls"></div>
</div>
-
节点关系:
<div id="container">
是<div class="display">
的 父节点 (Parent)。<div class="display">
是<div id="container">
的 子节点 (Child)。<div class="display">
和<div class="controls">
互为 兄弟节点 (Siblings)。
-
节点类型:
- 元素节点 (Element Node): HTML 标签,如
<div>
,<p>
,<span>
。 - 文本节点 (Text Node): 标签内的文字。
- 注释节点 (Comment Node): HTML 注释
<!-- ... -->
。 document
对象本身也是一个节点,是整棵 DOM 树的根节点。
- 元素节点 (Element Node): HTML 标签,如
查找与定位节点
CSS 选择器
现代 DOM API 提供了强大的、类似 CSS 选择器的方式来查找元素,这是最常用和推荐的方法。
// 返回匹配选择器的第一个元素
const mainContainer = document.querySelector("#container");
const firstButton = document.querySelector(".btn");
// 返回匹配选择器的所有元素,结果是一个静态的 NodeList
const allButtons = document.querySelectorAll(".btn");
const nestedDivs = document.querySelectorAll("#container > div");
querySelectorAll
返回的是 NodeList,不是真正的数组。虽然它有 forEach
方法且可以用下标访问,但缺少 map
, filter
等数组方法。需要时可轻松转换:
const buttons = document.querySelectorAll("button");
// 方法一: Array.from()
const buttonArray = Array.from(buttons);
// 方法二: 展开运算符 (...)
const buttonArray2 = [...buttons];
传统选择器
这些是较老的方法,在特定场景下仍然有用,尤其是 getElementById
,因为它的性能很高。
// 通过 ID 获取,速度最快,返回单个元素
const container = document.getElementById("container");
// 通过类名获取,返回一个实时的 HTMLCollection
const displays = document.getElementsByClassName("display");
// 通过标签名获取,返回一个实时的 HTMLCollection
const allDivs = document.getElementsByTagName("div");
getElementsByClassName
和 getElementsByTagName
返回的是一个实时的HTMLCollection。这意味着当文档中的元素发生变化时(例如通过脚本添加或删除了一个 div
),这个 HTMLCollection
的内容会自动更新。而 querySelectorAll
返回的 NodeList
是静态的 (static),它是在你调用它那一刻的快照,不会自动更新。
关系选择器
我们还可以从一个已知元素出发,查找其周围的亲属元素:
const controlsDiv = document.querySelector(".controls");
// 父元素
const parent = controlsDiv.parentElement; // -> <div id="container">
// 子元素
const containerDiv = document.querySelector("#container");
const children = containerDiv.children; // -> HTMLCollection [.display, .controls]
const firstChild = containerDiv.firstElementChild; // -> <div class="display">
const lastChild = containerDiv.lastElementChild; // -> <div class="controls">
// 兄弟元素
const displayDiv = document.querySelector(".display");
const nextSibling = displayDiv.nextElementSibling; // -> <div class="controls">
const prevSibling = controlsDiv.previousElementSibling; // -> <div class="display">
DOM 元素的增删改
创建与添加元素
// 1. 创建一个新元素
const newDiv = document.createElement("div");
// 2. (可选) 添加内容和属性
newDiv.textContent = "我是新来的!";
newDiv.classList.add("new-item");
// 3. 将其添加到 DOM 中
const container = document.getElementById("container");
// 作为最后一个子元素添加
container.appendChild(newDiv);
// 现代方法:更灵活
const p1 = document.createElement("p");
const p2 = document.createElement("p");
container.append(p1, "这是文本", p2); // 可以添加多个节点和文本
container.prepend("我是开头的文本"); // 在所有子元素之前添加
插入元素
// 在某个已知子元素之前插入
const container = document.getElementById("container");
const controls = document.querySelector(".controls");
const newSpan = document.createElement("span");
container.insertBefore(newSpan, controls); // 在 .controls 元素前插入 newSpan
// 现代方法:更直观
const display = document.querySelector(".display");
const newParagraph = document.createElement("p");
display.before("放在 display 前面"); // 作为兄弟节点插入在 display 之前
display.after(newParagraph, "放在 display 后面"); // 作为兄弟节点插入在 display 之后
删除与替换元素
// 传统方法
const container = document.getElementById("container");
const display = document.querySelector(".display");
container.removeChild(display);
// 现代方法:更简单
const controls = document.querySelector(".controls");
controls.remove(); // 直接在元素自身上调用 remove()
// 替换元素
const newH1 = document.createElement("h1");
newH1.textContent = "标题";
container.replaceChild(newH1, newDiv); // 用 newH1 替换 newDiv
修改元素内容
const div = document.createElement("div");
// 1. 修改纯文本 (推荐, 安全)
div.textContent = "Hello World!";
// 2. 读写 HTML (有 XSS 安全风险, 仅在信任内容时使用)
div.innerHTML = "<span>Hello <strong>World!</strong></span>";
修改属性
const link = document.createElement("a");
// 1. 直接通过属性名 (推荐, 简洁)
link.href = "https://www.google.com";
link.id = "myLink";
link.className = "link active"; // 注意 class 是用 className
// 2. 使用 setAttribute / getAttribute (通用, 适用于自定义属性)
link.setAttribute("target", "_blank");
const target = link.getAttribute("target");
link.removeAttribute("target");
// 3. 操作 data-* 自定义属性 (推荐)
link.dataset.userId = "12345"; // 对应 HTML: data-user-id="12345"
console.log(link.dataset.userId); // "12345"
data-*
属性和普通属性的区别如下:
- 普通属性 (
id
,class
,href
等): 通常是 HTML 的标准属性,它们有明确的、预定义的含义,浏览器会根据这些属性来渲染页面或执行特定功能(比如 href 用于链接跳转,src 用于加载图片)。 dataset
(data-*
属性): 是一个专门用来给你存放自定义数据的地方。它是一个官方推荐的、标准化的机制,用于将脚本所需的数据附加到元素上,而不会影响元素的样式和行为。这些属性对浏览器来说是透明的,浏览器会完全忽略它。
修改样式与类
const div = document.querySelector(".display");
// 1. 修改内联样式 (style)
// 注意:CSS 属性名中的短横线 (-) 要写成驼峰式 (camelCase)
div.style.color = "blue";
div.style.backgroundColor = "lightgray";
div.style.paddingLeft = "10px";
// 2. 操作类 (classList) (推荐, 最佳实践)
div.classList.add("active"); // 添加类
div.classList.remove("display"); // 移除类
div.classList.toggle("hidden"); // 如果类存在则移除,不存在则添加
const isActive = div.classList.contains("active"); // 检查是否存在类
事件处理
事件监听与事件对象
当用户与页面交互时(如点击、滚动、按键),浏览器会触发事件。我们可以编写代码来监听这些事件并做出响应。
const btn = document.querySelector("#btn");
function handleClick(event) {
alert("Hello World");
// `event` 对象包含了事件的所有信息
console.log(event.type); // 事件类型, e.g., "click"
console.log(event.target); // 触发事件的原始元素 (事件源)
console.log(event.currentTarget); // 事件监听器所在的元素 (这里是 btn)
// 阻止事件的默认行为 (例如,阻止 a 标签跳转,或 form 提交)
event.preventDefault();
}
// 添加事件监听器
btn.addEventListener("click", handleClick);
// 移除事件监听器 (函数必须是同一个引用)
btn.removeEventListener("click", handleClick);
事件流:冒泡与捕获
当一个事件在某个元素上发生时,它并不是立即结束的。它会经历一个完整的生命周期,分为三个阶段:
- 捕获阶段 (Capturing Phase): 事件从
window
开始,逐级向下传播到目标元素。 - 目标阶段 (Target Phase): 事件到达目标元素
event.target
。 - 冒泡阶段 (Bubbling Phase): 事件从目标元素开始,逐级向上传播回
window
。
默认情况下,addEventListener
在冒泡阶段处理事件。我们可以通过第三个参数来控制:
// 默认在冒泡阶段触发
element.addEventListener("click", handler);
// 等同于
element.addEventListener("click", handler, false);
// 在捕获阶段触发
element.addEventListener("click", handler, true);
事件委托
利用事件冒泡的原理,我们可以实现一种高效的模式——事件委托(Event Delegation)。当有大量子元素需要处理相同类型的事件时(例如一个长列表的每一项),我们不必为每个子元素都绑定监听器,而是只在它们的共同父元素上绑定一个监听器。
当子元素被点击时,事件会“冒泡”到父元素,触发父元素的监听器。然后,我们可以通过 event.target
来判断到底是哪个子元素触发了事件。
事件委托有如下的优点:
- 性能更高: 只需一个监听器,减少内存占用。
- 动态适应: 新增的子元素(例如通过 JS 添加的)无需重新绑定事件,它们会自动拥有事件处理能力。
const list = document.getElementById("my-list");
list.addEventListener("click", (event) => {
// 检查被点击的元素是否是我们关心的 `<li>` 元素
if (event.target && event.target.tagName === "LI") {
console.log("列表项被点击了:" + event.target.textContent);
event.target.style.color = "red";
}
});
常见事件类型
JavaScript 的常见事件类型如下:
- 鼠标事件:
click
,dblclick
,mousedown
,mouseup
,mousemove
,mouseover
,mouseout
,mouseenter
,mouseleave
- 键盘事件:
keydown
,keyup
,keypress
(已不推荐) - 表单事件:
submit
,input
,change
,focus
,blur
- 窗口与文档:
DOMContentLoaded
,load
,resize
,scroll
高级技巧与性能
DocumentFragment
当需要向 DOM 中添加大量元素时,如果逐个 appendChild
,每次都会触发一次重排 (reflow) 和重绘 (repaint),非常影响性能。
DocumentFragment
是一个轻量级的“文档片段”,可以把它看作一个存在于内存中的临时 DOM 容器。我们可以先把所有要添加的新元素都放进这个片段里,最后再一次性地将整个片段 appendChild
到真实 DOM 中。这样做只会触发一次重排和重绘。
const list = document.getElementById("my-list");
const fruits = ["苹果", "香蕉", "橙子", "葡萄"];
// 1. 创建一个文档片段
const fragment = document.createDocumentFragment();
fruits.forEach(fruitText => {
const li = document.createElement("li");
li.textContent = fruitText;
// 2. 先将新元素添加到片段中
fragment.appendChild(li);
});
// 3. 最后一次性将整个片段添加到真实 DOM
list.appendChild(fragment);
元素尺寸与位置
获取元素的几何信息对于实现动画、拖拽、响应式布局等功能至关重要。
const div = document.querySelector(".display");
// 获取元素的大小和相对于视口(viewport)的位置
const rect = div.getBoundingClientRect();
console.log(rect.width); // 元素的宽度
console.log(rect.height); // 元素的高度
console.log(rect.top); // 元素上边缘相对于视口顶部的距离
console.log(rect.left); // 元素左边缘相对于视口左侧的距离
console.log(rect.right);
console.log(rect.bottom);
// 获取包含边框和内边距的尺寸
const offsetWidth = div.offsetWidth;
const offsetHeight = div.offsetHeight;
// 获取滚动相关信息
const scrollTop = document.documentElement.scrollTop; // 整个页面的垂直滚动距离
3. JavaScript 对象
JavaScript 对象是一种将属性(键值对)和方法(函数)捆绑在一起的复合数据结构。它几乎是 JavaScript 中所有事物的核心。
对象的创建
对象字面量
对象字面量 (Object Literal) 是最常用、最简洁的创建方式。
const person = {
name: "张三",
age: 30,
hobbies: ["篮球", "音乐"],
// ES6 方法简写
greet() {
console.log("你好,我是" + this.name);
}
};
构造函数
构造函数 (Constructor Function) 通过 new
关键字调用一个函数来创建特定类型的对象实例。这是一种实现“类”的传统方式。
function Car(brand, model, year) {
this.brand = brand;
this.model = model;
this.year = year;
}
// 通过原型共享方法,节省内存
Car.prototype.displayInfo = function() {
console.log(`车辆信息: ${this.year} ${this.brand} ${this.model}`);
}
const myCar = new Car("Ford", "Mustang", 1969);
myCar.displayInfo(); // 输出: 车辆信息: 1969 Ford Mustang
Object.create()
Object.create()
创建一个新对象,并使用一个现有对象作为新对象的原型。这对于实现原型式继承非常有用。
const animal = {
isAlive: true,
breathe() {
console.log("呼吸中...");
}
};
const rabbit = Object.create(animal);
rabbit.name = "兔子";
console.log(rabbit.isAlive); // true (继承自 animal)
rabbit.breathe(); // "呼吸中..." (继承自 animal)
属性的访问、修改与删除
访问属性
Javascript 有三种访问属性的方法:
// 1. 点表示法 (Dot Notation)
// 静态、可读性好,是首选方式
console.log(person.name);
// 2. 方括号表示法 (Bracket Notation)
// 动态、灵活,当属性名是变量或包含特殊字符时使用
console.log(person["age"]);
const propName = "hobbies";
console.log(person[propName]); // ["篮球", "音乐"]
修改与添加属性
直接对已存在或不存在的属性赋值即可。
// 修改现有属性
person.age = 31;
// 添加新属性
person.email = "zhangsan@example.com";
person["home-address"] = "未知"; // 包含特殊字符,必须用方括号
删除属性
使用 delete
操作符可以从对象中彻底删除一个属性。
delete person.hobbies;
console.log(person.hobbies); // undefined
对象的遍历
for…in 循环
遍历对象中所有可枚举 (enumerable) 的属性,包括其原型链上的属性。
for (const key in person) {
// 使用 hasOwnProperty 确保只处理对象自身的属性
if (Object.prototype.hasOwnProperty.call(person, key)) {
console.log(`${key}: ${person[key]}`);
}
}
注意:
for...in
通常不被推荐用于遍历对象,因为它会带来原型链的干扰。推荐使用下面的Object
方法。
Object.keys(), Object.values(), Object.entries()
这三个方法都只返回对象自身的、可枚举的属性,不包含原型链上的属性,并且返回的是数组,可以方便地使用 forEach
, map
等数组方法。
// 获取所有键的数组
const keys = Object.keys(person);
// ["name", "age", "greet", "email", "home-address"]
// 获取所有值的数组
const values = Object.values(person);
// ["张三", 31, function, "zhangsan@example.com", "未知"]
// 获取所有 [键, 值] 对的数组
const entries = Object.entries(person);
// [["name", "张三"], ["age", 31], ...]
// 推荐的遍历方式
entries.forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
this
关键字
this
是 JavaScript 中一个复杂但至关重要的概念。它的值取决于函数是如何被调用的,而不是在哪里被定义的:
- 在方法中:
this
指向调用该方法的对象。person.greet()
中的this
就是person
。 - 在普通函数中: 在非严格模式下,
this
指向全局对象 (window
或global
);在严格模式 ('use strict'
) 下是undefined
。 - 在构造函数中:
this
指向正在创建的新实例。 - 在箭头函数中: 箭头函数没有自己的
this
,它会捕获其定义时所在上下文的this
值。这是它与普通函数最大的区别。
Getter 与 Setter
Getter 和 Setter 允许你像访问普通属性一样,但在背后执行一个函数来获取或设置值,从而增加逻辑控制。
const user = {
firstName: "John",
lastName: "Doe",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
const parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(user.fullName); // "John Doe" (调用 get)
user.fullName = "Jane Smith"; // 调用 set
console.log(user.firstName); // "Jane"
ES6+ 常用特性
属性和方法简写
const name = "Alice";
const age = 25;
const user = {
// 属性名和变量名相同时可以简写
name,
age,
// 方法简写
sayHi() {
console.log("Hi!");
}
};
计算属性名
允许在对象字面量中使用变量作为属性名。
const propKey = "dynamic_prop";
const obj = {
[propKey]: "This is a dynamic property"
};
console.log(obj.dynamic_prop); // "This is a dynamic property"
展开语法 (…) 与剩余属性
- 展开 (Spread): 用于合并或克隆对象。
- 剩余 (Rest): 用于在解构时收集余下的属性。
// 展开:合并对象
const defaults = { theme: "light", version: "1.0" };
const settings = { theme: "dark", showAvatar: true };
const finalSettings = { ...defaults, ...settings };
// { theme: "dark", version: "1.0", showAvatar: true }
// 剩余:解构
const { name, ...rest } = person;
console.log(name); // "张三"
console.log(rest); // { age: 31, greet: f, ... }
重要的内置 Object 方法
Javascript 的对象有下面的内置方法:
Object.assign(target, ...sources)
: 将一个或多个源对象的属性复制到目标对象,常用于合并对象(浅拷贝)。Object.freeze(obj)
: 冻结一个对象。冻结后不能添加、删除、修改任何属性。Object.seal(obj)
: 封闭一个对象。封闭后可以修改现有属性的值,但不能添加或删除属性。
4. 异步编程
JavaScript 是一门单线程语言,这意味着在任何给定时刻,它只能执行一个任务。如果某个任务是耗时操作(例如:网络请求、文件读写),那么整个应用程序的主线程都会被阻塞,导致页面无响应(卡死)。
为了解决这个问题,JavaScript 采用了异步非阻塞的模型,其核心就是事件循环 (Event Loop)。
事件循环
事件循环 (Event Loop) 是 JavaScript 能够处理高并发和异步操作的关键。它由以下几个核心部分组成:
- 调用栈 (Call Stack): 一个后进先出 (LIFO) 的数据结构,用于追踪当前正在执行的函数。
- Web APIs: 由浏览器提供的 API(或 Node.js 环境提供的 C++ API),用于处理像
setTimeout
,fetch
这样的异步操作。这些操作在后台运行,不会阻塞调用栈。 - 任务队列 (Task Queue): 分为两种:
- 宏任务队列 (Macro-task Queue): 用于存放
setTimeout
,setInterval
, I/O 操作, UI 渲染等的回调函数。 - 微任务队列 (Micro-task Queue): 用于存放
Promise
的回调 (.then
,.catch
,.finally
),queueMicrotask
等。微任务的优先级高于宏任务。
- 宏任务队列 (Macro-task Queue): 用于存放
事件循环的执行顺序如下:
- 执行调用栈中的所有同步代码。
- 当调用栈为空时,检查微任务队列。如果微任务队列不为空,则清空整个微任务队列,将其中的回调函数依次推入调用栈执行。
- 微任务队列清空后,检查宏任务队列。如果宏任务队列不为空,则取一个宏任务的回调函数推入调用栈执行。
- 一个宏任务执行完毕后,再次回到第 2 步,检查微任务队列。
- 重复以上过程,形成循环。
console.log('1. 同步代码开始');
setTimeout(() => {
console.log('5. setTimeout (宏任务)');
}, 0);
new Promise((resolve) => {
console.log('2. Promise 构造函数 (同步)');
resolve();
})
.then(() => {
console.log('4. Promise.then (微任务)');
});
console.log('3. 同步代码结束');
// 输出顺序: 1 -> 2 -> 3 -> 4 -> 5
回调函数
回调函数 (Callbacks) 是异步编程最基础的形式。它是一个作为参数传递给另一个函数的函数,在异步操作完成后被“回调”执行。
当多个异步操作相互依赖时,容易形成“回调地狱 (Callback Hell)”,代码横向发展,难以阅读和维护。
// 模拟一个需要回调的异步函数
function step(number, callback) {
setTimeout(() => {
const result = number + 1;
console.log(`步骤 ${number} 完成,结果: ${result}`);
callback(result);
}, 1000);
}
// 回调地狱
step(1, (result1) => {
step(result1, (result2) => {
step(result2, (result3) => {
console.log("所有步骤完成!");
});
});
});
Promise
Promise 是 ES6 引入的,用于优雅地处理异步操作。它是一个代表了异步操作最终完成或失败的对象。
- 三种状态:
pending
(进行中),fulfilled
(已成功),rejected
(已失败)。
Promise 的状态一旦改变,就不会再变。
Promise 的链式调用
Promise 通过 .then()
的链式调用解决了回调地狱的问题。.then()
方法本身会返回一个新的 Promise,这使得链式调用成为可能。
- 如果
.then()
的回调函数返回一个值,那么新的 Promise 会立即变为fulfilled
状态,并且该值会作为下一个.then()
的输入。 - 如果
.then()
的回调函数返回一个新的 Promise,那么整个链条会等待这个新的 Promise 完成,然后将其结果传递给下一个.then()
。
function step(number) {
return new Promise((resolve) => {
setTimeout(() => {
const result = number + 1;
console.log(`步骤 ${number} 完成,结果: ${result}`);
resolve(result);
}, 1000);
});
}
step(1)
.then(result1 => step(result1)) // 返回一个新的 Promise
.then(result2 => step(result2))
.then(result3 => {
console.log("所有步骤完成!");
return "最终结果"; // 返回一个普通值
})
.then(finalResult => {
console.log(finalResult); // "最终结果"
})
.catch(error => {
console.error("某个步骤出错了: ", error);
});
Promise 的静态方法
Promise 有如下的静态方法
-
Promise.all(promises)
: 并行执行多个 Promise。当所有 Promise 都成功时,它才会成功,并返回一个包含所有结果的数组。如果任何一个 Promise 失败,它会立即失败,并返回那个失败的原因。- 场景:需要同时获取多个无关的数据才能进行下一步操作。
-
Promise.race(promises)
: “赛跑”。返回第一个完成(无论是成功还是失败)的 Promise 的结果。- 场景:给一个请求设置超时时间。我们可以创建一个超时 Promise。
-
Promise.allSettled(promises)
: 等待所有 Promise 都完成(无论是成功还是失败),然后返回一个包含每个 Promise 结果对象({status, value}
或{status, reason}
)的数组。- 场景:需要知道一组异步操作中每一个的结果,即使其中有失败的。
-
Promise.any(promises)
: 返回第一个成功的 Promise 的结果。如果所有 Promise 都失败了,则会抛出一个AggregateError
。- 场景:从多个源获取同一个资源,哪个先回来用哪个。
// Promise.all 示例
const promise1 = fetch('/api/user/1');
const promise2 = fetch('/api/user/2');
Promise.all([promise1, promise2])
.then(responses => Promise.all(responses.map(res => res.json())))
.then(users => {
console.log('获取到两个用户:', users);
})
.catch(error => {
console.error('获取用户失败:', error);
});
Async/Await
async/await
是基于 Promise 的语法糖,它让异步代码在写法上几乎与同步代码无异,是目前最推荐的异步编程方式。
相关概念
async
函数:async
关键字用于声明一个异步函数。async
函数的返回值总是一个 Promise。await
操作符:await
用于等待一个 Promise 完成。它只能在async
函数内部使用。它会“暂停”当前async
函数的执行,但不会阻塞主线程,等待 Promise 解决后,将fulfilled
的值返回。- 错误处理: 使用标准的
try...catch
语句来捕获await
期间 Promise 的rejection
。
async function fetchUserAndPosts(userId) {
try {
console.log('开始获取用户...');
const userResponse = await fetch(`/api/user/${userId}`);
const user = await userResponse.json();
console.log(`获取到用户: ${user.name},开始获取他的文章...`);
const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();
console.log(`获取到 ${posts.length} 篇文章`);
return { user, posts };
} catch (error) {
console.error('获取数据失败:', error);
// 向上抛出错误,让调用者也能处理
throw error;
}
}
// 调用 async 函数
fetchUserAndPosts(1)
.then(data => console.log('最终数据:', data))
.catch(error => console.log('在调用链中捕获到错误'));
并行执行任务
一个常见的错误是连续 await
多个独立的异步操作,这会导致它们串行执行,效率低下。
// 错误示范:串行执行,总耗时约等于 t1 + t2
async function getTwoUsersWrong() {
const user1 = await fetch('/api/user/1'); // 等待这个完成
const user2 = await fetch('/api/user/2'); // 再执行这个
}
// 正确示范:使用 Promise.all 并行执行,总耗时约等于 max(t1, t2)
async function getTwoUsersRight() {
const [user1Response, user2Response] = await Promise.all([
fetch('/api/user/1'),
fetch('/api/user/2')
]);
const user1 = await user1Response.json();
const user2 = await user2Response.json();
return [user1, user2];
}
顶层 Await
在 ES 模块的顶层作用域中,可以直接使用 await
,而无需将其包裹在 async
函数中。这在初始化和加载依赖时非常方便。
// 在一个 .js 模块文件中 (type="module")
const initialData = await fetch('/api/initial-data');
const config = await initialData.json();
export function setup() {
// 使用已加载的 config
}