以下是本人在 js 学习过程中个人总结和积累的一些笔记,我学习或参考过 黑马js、渡一、JavaScript高级程序设计、MDN、al 交流等课程或资料,希望可以帮助到读者,欢迎读者纠偏
本篇是 JS 进阶篇 三,包括 canvas、Promise、异步函数、迭代器和生成器、深入 DOM 等
Canvas
画布元素(canvas)是 HTML5 的一部分,允许对 2D 和 3D 形状以及位图进行动态、可脚本化地渲染
创建 Canvas
在网页中,使用 <canvas> 标签创建 canvas
// 如果不设置 canvas 元素的 width 和 height 属性,则它默认宽高为 300 * 150
<canvas width="300" height="150"></canvas>
分辨率和画布大小
通过 html 属性设置的宽高是 cavas 的分辨率,即画布自身的逻辑像素坐标系 ,它作用于画布绘画栅格本身,值为无单位的数字,如 300 * 150 的分辨率,代表该 canvas 具有 300 * 150 个绘画单位
通过 css 设置的宽高是 <canvas> 标签的宽高,即画布元素在网页上显示的物理尺寸(以 CSS 像素为单位),它作用于 <canvas> 这个 HTML 盒模型,其默认值为容器的内部尺寸,如果它的比例跟内部尺寸不同,会导致 canvas 的内容被拉伸或者挤压变形,大小跟内部尺寸不同时还会导致模糊
可以将 canvas 内部尺寸视为一张照片,而 css 尺寸则是装照片的容器
实例属性
| 属性 | 含义 |
|---|---|
| width | canvas 的横向分辨率 |
| height | 纵向分辨率 |
实例方法
| 方法 | 主要作用与功能 | 关键参数 | 返回值/说明 |
|---|---|---|---|
getContext() | 获取渲染上下文,这是所有绘图操作的起点和核心 | contextType: 如 '2d', 'webgl' | 返回一个绘图上下文对象(如 CanvasRenderingContext2D) |
toDataURL() | 将画布导出为一张图片的Base64编码数据URL | type (可选): 图片格式,如 'image/png';quality (可选): 0-1的质量系数 | 一个包含图像数据的字符串,可直接用作 img.src 或下载链接 |
toBlob() | 将画布导出为一个Blob二进制数据对象,适合上传或处理大图 | callback: 接收Blob对象的回调函数;type, quality 同 toDataURL | 异步操作,结果在回调函数的 Blob 参数中 |
captureStream() | 捕获画布内容的实时视频流,可用于直播、录屏或视频会议 | frameRate (可选): 指定视频流的帧率 | 返回一个 MediaStream 对象,可作为 video.srcObject 或 WebRTC 源 |
transferControlToOffscreen() | 将控制权转移给一个离屏Canvas,用于在Web Worker线程中绘图以避免阻塞主线程 | 无参数 | 返回一个 OffscreenCanvas 对象,需在Worker中配合 getContext 使用 |
保证 canvas 的内容清晰
- 核心
物理尺寸 = 逻辑尺寸 * 缩放倍率(DPI)
物理像素 = 逻辑像素 * 缩放倍率
[!tip] 元素如图片 canvas 等设置的 height width 是物理尺寸,css 设置的是逻辑尺寸
在普通屏幕中,一个逻辑像素对于一个物理像素,而在高清屏幕中,一个逻辑像素会对应多个物理像素,而物理尺寸和逻辑尺寸的比值称为 dpi ,在 JS 中可以使用 window.devicePixelRatio 获取当前 dpi
- 示例 —— 一个不会糊的圆环
<script setup>
import { useTemplateRef, onMounted } from 'vue'
const canvas = useTemplateRef('canvas')
function draw() {
const dpr = devicePixelRatio
canvas.value.width = 400 * dpr
canvas.value.height = 300 * dpr
const ctx = canvas.value.getContext('2d')
//
ctx.scale(dpr, dpr)
ctx.strokeStyle = '#fff'
ctx.lineWidth = 10
ctx.beginPath()
ctx.arc(200, 150, 100, 0, Math.PI * 2)
ctx.stroke()
}
onMounted(() => {
draw()
})
window.onresize = () => {
draw()
}
</script>
<template>
<canvas ref="canvas" />
</template>
<style scoped>
canvas {
width: 400px;
height: 300px;
background-color: black;
}
</style>
Canvas —— 2D
获取绘制上下文
<template>
<canvas />
</template>
<script>
const canvas = document.querySelector('#canvas')
const cxt = canvas.getContext('2d')
</script>
绘制矩形
| 方法名 | 方法含义 | 参数含义 | 注意点 | 示例 |
|---|---|---|---|---|
fillRect(x, y, w, h) | 绘制一个填充的矩形 | x, y: 矩形左上角坐标w, h: 矩形宽度和高度 | 使用当前的 fillStyle | ctx.fillRect(10, 10, 100, 50); |
strokeRect(x, y, w, h) | 绘制一个描边的矩形 | 同上 | 使用当前的strokeStyle和lineWidth | ctx.strokeRect(125, 10, 100, 50); |
clearRect(x, y, w, h) | 清除指定矩形区域, 使之完全透明 | 同上 | 常用于动画中清空画布或部分区域 | ctx.clearRect(15, 15, 90, 40); |
绘制圆形/圆弧
// 语法
ctx.arc(x, y, r, startAngle, endAngle, anticlockwise)
// 示例
ctx.arc(100, 75, 30, 0, Math.PI * 2);
| 参数 | 含义 | 备注 |
|---|---|---|
| x | 圆心坐标 | |
| y | 圆心坐标 | |
| r | 半径 | |
| startAngle | 起始弧度 | 从 x 轴正半轴开始计算,如下图所示 |
| endAngle | 结束弧度 | |
| anticlockwise | 绘制顺序 | true —— 顺时针 | false —— 逆时针(默认值) |
![[z_attachments/Pasted image 20251202174944.png|300]]
绘制贝塞尔曲线
一次贝塞尔曲线(一个控制点)
![[z_attachments/Pasted image 20251202175233.png|300]]
// 语法
ctx.quadraticCurveTo(cp1x,cp1y,x,y)
二次贝塞尔曲线
![[z_attachments/Pasted image 20251202175340.png|300]]
// 语法
ctx.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)
绘制路径
路径方法需成对使用:beginPath() 开始,fill()或stroke()结束绘制
| 方法名 | 方法含义 | 参数含义 | 注意点 | 示例 |
|---|---|---|---|---|
beginPath() | 开始一条新的绘制路径 | 无 | 关键:开始新图形前调用,避免与之前路径混合 | ctx.beginPath(); |
moveTo(x, y) | 将画笔移动到画布上的指定点(不画线) | x, y: 目标点坐标 | 用于设置路径的起点 | ctx.moveTo(50, 50); |
lineTo(x, y) | 从画笔当前点画一条直线到目标点 | x, y: 目标点坐标 | 画笔终点会移动到目标点 | ctx.lineTo(150, 50); |
rect(x, y, w, h) | 向当前路径添加一个矩形路径(不直接绘制) | 同 fillRect | 与 fillRect 不同,它只是添加路径,需后续调用 fill() 或 stroke() | ctx.rect(10, 110, 80, 60); |
fill() | 填充当前路径的内部区域 | 无 | 使用当前的 fillStyle填充后路径默认不会保留 | ctx.fill(); |
stroke() | 描边当前路径 | 无 | 使用当前的 strokeStyle, lineWidth 等 | ctx.stroke(); |
closePath() | 将路径的起点和终点用直线连接,形成闭合路径 | 无 | 并非结束路径,而是闭合它之后仍可继续绘制或填充/描边 | ctx.closePath(); |
// 示例
const ctx = document.createElement('canvas')
ctx.beginPath()
ctx.moveTo(100, 100)
ctx.lineTo(400.200)
ctx.stroke()
ctx.closePath()
样式与颜色
以下是属性,不是方法,在对应绘制操作前设置
// 支持多种颜色模式
ctx.fillStyle = '#000'
ctx.fillStyle = 'black'
ctx.fillStyle = 'rgb(0, 0, 0)'
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)' ......
| 属性/方法 | 作用 | 值/值类型 | 示例 |
|---|---|---|---|
fillStyle = color | 设置或返回用于填充的颜色、渐变或图案 | 颜色字符串、CanvasGradient或CanvasPattern对象 | ctx.fillStyle = '#FF0000'; ctx.fillStyle = gradient; |
strokeStyle = color | 设置或返回用于描边的颜色、渐变或图案 | 同上 | ctx.strokeStyle = 'blue'; |
lineWidth = value | 设置或返回当前线条的宽度(单位:像素) | 数字,必须为正 | ctx.lineWidth = 5; |
lineCap | 设置线帽 | butt —— 无线帽(默认) round —— 圆线帽 square —— 方线帽 | |
lineJoin | 设置线连接处样式 | miter —— 延伸相邻部分外边缘(默认值) bevel —— 相连部分末端填充一个额外的以三角形为底的区域,每个部分都有各自独立的矩形拐角 round —— 填充一个圆心在相连部分末端的扇形,圆角的半径是线段的宽度 | |
setLineDash(segments) | 设置虚线样式 | 值为数组,[实线长度, 虚线长度.......] ,按照实线、虚线的顺序书写,遍历完后会再次重复 | ctx.setLineDash([2, 1]) |
[!tip] 注意
- fillStyle 默认为黑色
- 在修改 fillStyle 的值之前,它的所有绘制都是该颜色(参考真实的画笔)
lineJoin 样式,分别为 round , bevel , miter
![[z_attachments/Pasted image 20251202191029.png|300]]
文本绘制
属性
| 属性 | 含义 | 属性值 |
|---|---|---|
| font | 字体属性 | 同 css font 属性 |
| textAlign | 文本对齐 | left , right , center , start , end |
| textBaseLine | 基线对齐 | top , hanging , middle , alphabetic , ideographic , bottom |
| direction | 文字方向 | ltr , rtl , inherit |
方法
若超过最大宽度,文字会被挤压变形
// 绘制填充文字
fillText(text, x, y[, maxWidth])
// 示例
ctx.fillText('Hello', 50, 150);
// 绘制描边文字
strokeText(text, x, y[, maxWidth])
// 示例
ctx.strokeText('World', 50, 180);
// 获取文字的度量信息(如宽度等)
ctx.measureText(text)
// 示例
const text = ctx.measureText('hello world')
console.log(text.width)
| 参数 | 含义 | 备注 |
|---|---|---|
| text | 文本内容 | |
| x | 文本起始 x 坐标 | 设定的是基线左下角 |
| y | 文本起始 y 坐标 | 设定的是基线左下角 |
图像绘制
[!tip] 注意
- 以下采用同一个示例
创建图片对象
创建一个 HTML image element ,添加图像 src
const img = new Image()
img.src = 'my-img.jepg'
绘制图片
图像的加载是异步的,需确保图像加载完成后再调用drawImage
| 方法名 | 方法含义 | 参数含义 | 示例 |
|---|---|---|---|
drawImage(image, x, y[, width, height]) | 在画布上定位绘制图像 | image: 图像源 x, y: 图片左上角坐标 | ctx.drawImage(img, 200, 10, 200, 200); |
// 示例
img.onload = function () {
ctx.drawImage(img, 0, 0)
}
裁剪图片
img.drawImage(img,sx,sy,sWidth, sHeight, dx, dy, dWidth, dHeight)
| 参数 | 含义 | 备注 |
|---|---|---|
| img | 图片源 | img 对象 |
| sx | 裁剪图片的 x 坐标 | 坐标原点为源图片左上角 |
| sy | 裁剪图片的 y 坐标 | 坐标原点为源图片左上角 |
| sWidth | 裁剪图片在源图片中宽度 | |
| sHeight | 裁剪图片在源图片中高度 | |
| dx | 放置裁剪图片的 x 坐标 | 坐标原点为 canvas 画布左上角 |
| dy | 放置裁剪图片的 y 坐标 | 坐标原点为 canvas 画布左上角 |
| dWidth | 裁剪图片在画布中的宽度 | |
| dHeight | 裁剪图片在画布中的宽度 | |
| ![[z_attachments/Pasted image 20251202182051.png | 300]] ![[z_attachments/Pasted image 20251202182112.png | 300]] |
getImageData() —— 读取像素数据
返回一个 ImageData 对象
ctx.getImageData(sx, sy, sw, sh[, settings])
| 参数 | 含义 | 备注 |
|---|---|---|
| sx | 要提取 ImageData 的矩形左上角的 x 轴坐标 | |
| sy | 要提取 ImageData 的矩形左上角的 y 轴坐标 | |
| sw | 要提取 ImageData 的矩形的宽度 | 正值向右延伸,负值向左延伸 |
| sh | 要提取 ImageData 的矩形的高度 | 正值向下延伸,负值向上延伸 |
| settings | 一个配置对象,有属性 colorSpace | colorSpace:指定图像数据的颜色空间 可以设置为 "srgb" 表示 sRGB 色彩空间,或 "display-p3" 表示 display-p3 色彩空间 |
ImageData 对象具有一个重要属性 —— data ,它是一个数组,代表每个像素点的颜色值(每个像素点有4个颜色值 rgba ,颜色值的范围为 0-255)
// 示例
const imageData = new ImageData(100, 100, 200, 400)
console.log(imageData.data) // [255, 255, 0, 255......] 第一个像素点的颜色,这里的第四个值 255 表示透明度,这里为不透明
当一个 ImageData 对象的范围为整个 canvas 画布时,则有以下关系
// 像素点颜色起始索引 index 坐标 x, y, canvas 宽度 width
index = (y * width + x) * 4
![[z_attachments/Pasted image 20251202202158.png|400]]
获取了坐标对应的像素索引后,可以根据像素信息修改像素点颜色
// startIndex 为起始索引,数组参数为一个像素点信息数组,如 [155, 230, 244, 255]
imageData.data.set([], startIndex)
putImageData() —— 将已有的 ImageData 绘制到画布上
在前面的操作中,我们直接修改了 ImageData 的像素点信息,但是还没有将它绘制出来,因此可以通过该 api 绘制
ctx.putImageData(imageData, x, y)
ctx.putImageData(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight)
| 参数 | 类型 | 是否可选 | 描述与作用 |
|---|---|---|---|
imageData | ImageData | 必选 | 包含像素值(RGBA数组)的数据对象,通常由 getImageData() 或 createImageData() 获得 |
dx | number | 必选 | 目标坐标X。imageData 矩形区域的左上角将被放置到画布的该X坐标处 |
dy | number | 必选 | 目标坐标Y。imageData 矩形区域的左上角将被放置到画布的该Y坐标处 |
dirtyX | number | 可选(默认0) | 源矩形起点X。定义从 imageData 数据本身的哪个X坐标开始选取 |
dirtyY | number | 可选(默认0) | 源矩形起点Y。定义从 imageData 数据本身的哪个Y坐标开始选取 |
dirtyWidth | number | 可选(同宽) | 源矩形宽度。定义从 dirtyX 开始,选取多宽的区域 |
dirtyHeight | number | 可选(同高) | 源矩形高度。定义从 dirtyY 开始,选取多高的区域 |
变换
变换会影响此后所有的绘制操作
| 方法名 | 方法含义 | 参数含义 | 需要注意的点 | 示例 |
|---|---|---|---|---|
translate(x, y) | 将画布原点平移到新位置 | x, y: 新原点相对于旧原点的偏移量 | 常用于方便地绘制重复或复杂图形 | ctx.translate(50, 50); |
rotate(angle) | 以当前原点为中心旋转坐标系 | angle: 旋转角度(弧度,顺时针为正) | 同样使用弧度制 | ctx.rotate(Math.PI / 4); |
scale(x, y) | 缩放画布 | x, y: 横轴和纵轴的缩放因子 | >1放大,0~1缩小,负值会翻转镜像影响后续所有坐标和线条宽度 | ctx.scale(2, 0.5); |
状态管理
用于保存和恢复绘图环境(样式、变换等),是极其重要的功能
| 方法名 | 方法含义 | 需要注意的点 | 示例 |
|---|---|---|---|
save() | 将当前的绘图状态压入堆栈保存 | 保存的是“状态快照”,不包括已绘制的像素内容 | ctx.save(); // 保存当前状态 |
restore() | 从堆栈中弹出并恢复最近一次保存的绘图状态 | 没有保存状态时调用会报错与save()配对使用 | ctx.restore(); // 恢复之前的状态 |
Promise —— 期约
Promise 是 JS 主导性的异步编程机制,是一个有状态的对象,其状态是私有的,无法被外部 JS 修改和直接检测,期约将异步行为封装起来,以避免根据读到的期约状态已同步方式处理期约对象
为了后续能方便看到 Promise 的结果,在这里封装一个函数
console.asynclog = (...args) => setTimeout(console.log,0,...args)
期约的初始化
Promise 通过 new 操作符进行初始化,初始化时必须传入执行器(executor)函数作为参数,否则 JS 解释器会抛出错误 —— SyntaxError
const promise = new Promise(() => {})
console.asynclog(promise) // Promise <pending>
期约的状态
Promise 具有三种状态
- pending - 待定
- fulfilled - 已兑现
- rejected - 已拒绝
待定是 Promise 的初始状态,表示程序待定或正在进行中,但还未返回结果,fufilled 表示期约已兑现,兑现的期约有一个私有的内部值,reject 表示期约已拒绝,被拒绝的期约具有一个私有的内部理由,两个私有值都是包含原始值或对象的不可修改的引用,二者是可选的,默认为 undefined
期约的状态一旦从 pendding 改变为 fufilled 或 reject ,就会被固定,这个过程不可逆,固定状态后尝试继续修改状态会静默失败
![[z_attachments/065a8cb28583a169edd5b58e28253afa.jpg|400]]
executor —— 执行器函数
- 描述 执行器函数是期约的初始化程序,期约的状态是私有的,只能在内部进行操作,内部操作在期约的执行器函数中完成,这个函数具有两项职责:
- 初始化期约的异步行为
- 控制状态的最终转换
期约状态的转换是通过调用执行器的两个函数实现的,即 resolve() 和 reject()
const executor = (resolve, reject) => {}
new Promise(executor)
[!tip] 注意
- 执行器函数是同步执行的
- 在执行器函数中抛出异常会导致期约被拒绝
const p = Promise.reject(Error('lcl'))
// 等价于
const p = new Promise((resolve, reject) => throw Error('lcl'))
console.asynclog(p) // Promise <rejected>: Error: lcl
面试考点
- 抛出异常改变 Promise 状态 在同步中抛出异常会被捕获,然后改变 Promise 状态为拒绝,在异步中 throw 错误不会被捕获,如下,异步回调时,执行器已经结束运行,执行上下文不是执行器,因此抛出错误不会改变 Promise 状态
而且,Promise 的执行器被隐式 try/catch 包裹,捕捉不了异步错误
// 1. 同步抛出
const p = new Promise((resolve, reject) => throw 3)
console.log(p) // Promise <rejected>
// 2. 异步抛出
const p = new Promise((resolve, reject) => {
setTimeout(() => { throw 3 }, 6)
})
console.log(p) // Promise <pedding>
setTimeout(() => {
console.log(p) // Promise <pedding>
}, 1000);
但是使用 resolve 和 reject 却可以异步改变状态,因为它们被传入内部的异步逻辑(即异步函数时,形成了闭包,一直持有 构造器的 resolve 和 reject 两个改变 Promise 状态的函数,因此无论何时,只要调用它们,就能改变这个新 promise 的状态,使用以下代码验证
let a
const p = new Promise(resolve => a = resolve)
console.log(p) // Promise {<pending>}
// a 获取到 resovle 函数
console.log(a) // ƒ () { [native code] }
// 调用 a ,改变 p 的状态
a(666)
console.log(p) // Promise {<fulfilled>: 666}
- 状态吸收
使用
new Promsie((resolve, reject) => { resolve() })时,如果传入 thenable 或 promise,则会导致这个过程变为异步
// 1. 传递 thenable
const p = new Promise(resolve => resolve({ then() {} }))
console.log(p) // Promise <pending>
// 2. 传递 promise
const p = new Promise(resolve => resolve(Promise.resolve(6)))
console.log(p) // Promise <pending>
// 3. 传递本身,导致p在未完成初始化之前调用,先返回一个 fufiiled 的promise 兑现值为报错内容,然后报错
const p = new Promise(resolve => resolve(p))
console.log(p) // ReferenceError: Cannot access 'p' before initialization
// 4. 传递本身,初始化后再传递,形成自指死循环
let a
const p = new Promise(resolve => a = resolve)
a(p) // TypeError: Chaining cycle detected for promise
- resolve 和 reject 的不对称性
reject是一个原子操作,它只做一件事,无论传递给reject什么值,它都会把它包装为该 promsie 的拒绝理由
// 阻止报拒绝期约未捕获错误
window.onunhandledrejection = (e) => {
e.preventDefault()
}
// 直接将Promise.reject(666)作为拒绝理由返回,对兑现期约和 thenable 也是一样
const p = new Promise((resolve,reject) => reject(Promise.reject(666)))
console.log(p) // Promise {<rejected>: Promise}
resolve(x) 的执行流程如下
-
自指检测 首先判断参数 x 是否是当前 Promise 自身,如果是,抛出 TypeError 并 Reject,防止死循环
-
类型检测与属性读取 判断 x 是否为对象或函数。
- 如果不是,或者尝试读取 x.then 时发现它不是函数,则直接以 x 为值 Fulfill 当前 Promise。
- 如果在读取 x.then 属性的过程中抛错,直接以该错误 Reject
-
Thenable 处理(核心)
- 如果 x.then 是函数,说明 x 是一个 Promise 或 Thenable。
- 为了 防止恶意同步 then 方法破坏异步一致性 以及 防止无限 Promise 链导致的栈溢出 ,引擎不会立即执行 这个 then 方法。
- 引擎会将 “执行 x.then(resolve, reject) ” 这个操作打包为一个 微任务 ,推入微任务队列。
- 等到该微任务执行时,才会真正调用 then 方法,并把控制权移交给 x ,实现状态的递归解包
// 一个坏坏的 Thenable
const evilThenable = {
then: function(resolve) {
// 嘿嘿,我不守规矩,我现在就立刻同步调用 resolve!
resolve('我是坏人');
}
};
// 若不将 then 打包重复的话,会导致,调用 then 方法的同时,promise 的状态就改变了,导致传入 thenable 时, new Promise 的状态有可能是 pendding 有可能是 fufilled ,导致不稳定
Promise.resovle() —— 实例化兑现期约
[!tip] 注意
- 该方法可以将任何非期约值包装为已兑现期约,包括错误对象
- 不传入参数时,其打印结果为
Promise <fufilled>: undefined- 若
Promsie.resovle()的参数直接抛出未捕获错误,那么会直接中断代码执行,并不会返回一个 Promise(如 console.log(Promise.resolve((() => {throw 3})())))
- 描述
期约并非开始必须处于 pendding 状态,使用 Promise.resolve() 可以实例化一个被兑现的期约
该方法接收一个参数,作为期约的兑现值
// 以下代码是等价的
const p1 = new Promise((resovle, reject) => resovle(param))
const p2 = Promise.resovle(param)
该方法是一个幂等方法,即如果传入的参数本身就是一个期约,那就直接返回该期约,不做任何包装,这个幂等性会保留传入期约的状态
// 1. 幂等方法
const val = 42;
const p1 = Promise.resolve(val);
const p2 = Promise.resolve(p1);
console.log(p1 === p2); // true传入期约时不做任何处理,直接返回
// 2. 传入期约,返回一个待定的期约
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功')
}, 10000)
})
const p3 = Promise.resolve(p) // Promise <pending>
面试考点
[!tip] 注意
- 之所以 thenable.then 推入微队列,是因为 thenable 的具体实现不同,不推入微队列则会导致 thenable.then 有可能是同步,有可能异步,引发了 Zalgo 效应(函数行为不可预测),而推入微队列则解决了这个问题,保证代码一定是异步执行的
// 示例
// 示例:thenable 对象
const thenable = {
then(resolve, reject) {
console.log('thenable.then 被调用');
resolve('结果');
}
};
// 情况1:如果是同步的
console.log('开始');
Promise.resolve(thenable).then(val => console.log(val));
console.log('结束');
// 输出顺序(实际,异步):
// 开始
// thenable.then 被调用
// 结束
// 结果
// 输出顺序(假设同步):
// 开始
// thenable.then 被调用
// 结果 ❌ 这里插入了异步逻辑
// 结束
- 处理步骤 —— 遇到 thenable 时是异步的 首先查看参数是否是原生 promsie ,通过查看它是否是 Promise 的实例,如果是的话,则为原生 promise ,直接返回该 promise,不做任何处理
如果不是 promise,则检测它的 then 属性,如果是一个函数,则说明它是一个 thenable ,那么创建一个新的 Promise ,将其 resolve 和 reject 参数传入 thenable.then 并将它推入微任务队列,然后根据实现 resolve 和 reject 的调用修改这个新 Promise 的值,最后返回这个新 Promise ,这就是 thenable 的解包,由于这一步创建了一个新的 Promise ,因此传入 thenable 对象时 Promise.reslve(thenable) 一开始是 pendding ,新 promise 状态改变后才返回对应的 promise
如果既不是 promise 也不是 thenable ,那么就直接将参数作为 Promise 的兑现值,返回兑现的 promise
const p1 = Promise.resolve('6')
const thenable = {
then(resolve, reject) {
resolve(666)
}
}
// p1 是 Promise 的实例
console.log(p1 instanceof Promise) // true
// thenable 不是 Promise 的实例
console.log(thenable instanceof Promise) // false
// 传入 thenable 导致需要解包,是一个异步的过程
const p2 = Promise.resolve(thenable)
console.log(p2) // Promise <pendding>
console.asyclog(p2) // Promise <fufilled>: 666
Promise.reject() —— 实例化拒绝期约
该方法会实例化一个拒绝的期约并抛出一个异步错误,该错误无法被 try/catch 捕获,只能通过拒绝处理程序捕获(.catch())
// 1. 下面两个期约等价
new Promise((resovle, reject) => reject(reason))
Promise.reject(reason)
// 2. 尝试使用 try/catch 捕获异步错误
try{
Promise.reject(new Error('this is a error'))
} catch(e) {
console.log(e)
}
// Uncaught (in promise) Error: this is a error
[!tip] 注意
- 该方法不是幂等的,他会对所有传入的参数进行包装
- 无法捕获错误的原因是,拒绝期约的错误并未被抛到执行同步代码的线程中,而是通过浏览器异步消息队列来处理的,因此 try/catch 无法捕获该错误
- 从第二点可以看出,代码一旦开始以异步模式执行,唯一能与之交互的方式就是使用异步结构,即期约的方法
- 不传入参数时,其打印结果为
Promise <reject>: undefined
const p1 = Promise.resovle(3)
const p2 = Promise.reject(p1)
console.asynclog(p2) // Promise <pendding>: Promise <fufilled>
Promise.prototype.then() —— 为期约添加处理程序
[!tip] 注意
- .then 方法本身是同步的,它会把它的两个处理程序同步加入到一个内部的列表,当 promise 状态落定后,处理程序会从内部列表中将对应的处理程序取出,放进微任务队列等待执行,关于这一点可以重写
Promise.prototype.then方法,添加日志打印到其中进行观察
- 参数 该方法接收两个可选参数,onFufilled 处理程序和 onRejected 处理程序,分别在期约进入“兑现”和“拒绝”状态时执行,这两个操作是互斥的,传递的非函数参数会被静默忽略
const p = new Promise((resolve, reject) => {
setTimeout(() => reject())
})
// onFuiiled 和 onReject 参数
p.then(onFufilled, onReject)
[!tip] 注意
- 以下内容都是针对 onFuilled 处理程序来说的,onReject 处理程序的内容放置在下一节 —— .catch() 语法糖中
- 返回值
该方法返回一个新期约实例,这个新实例基于 onFufilled 处理程序的返回值构建,即它通过
Promise.resovle()进行包装生成新期约,具体规则如下- 如果未提供 onFufilled 处理程序,则
Promise.resovle()包装上一个期约兑现之后的值 - 如果没有显式返回语句,则
Promise.resovle()包装默认返回值undefined - 如果有显式返回语句,则包装这个值
- 抛出异常会返回被拒绝的期约(但返回错误值不会)
- 如果未提供 onFufilled 处理程序,则
const p1 = Promise.resovle('lcl')
// 1. 不提供 onFufilled 程序
const p2 = p1.then()
console.asynclog(p2) // Promise <fufiiled>: lcl
// 2.不返回,不显式返回或返回 undefined
const p3 = p1.then(() => undefined);
const p4 = p1.then(() => {});
const p5 = p1.then(() => Promise.resolve());
console.asynclog(p3) // Promise <fufiiled>: undefined
console.asynclog(p4) // Promise <fufiiled>: undefined
console.asynclog(p5) // Promise <fufiiled>: undefined
// 3. 有显式的返回值
const p6 = p1.then(() => 'dhh')
const p7 = p1.then(() => Promise.resovle('dhh'))
console.asynclog(p6) // Promise <fufilled>: dhh
console.asynclog(p7) // Promise <fufilled>: dhh
// 4. 抛出异常返回被拒绝期约
const p8 = p1.then(() => { throw 'dhh' })
console.asynclog(p8) // Promise <rejected>: dhh
Promise.prototype.catch() —— 为期约添加拒绝处理程序
-
参数 接收一个函数参数,即 onRejected 处理程序,该方法本质为一个语法糖,相当于调用
Promise.prototype.then(null, onRejected) -
返回值 该方法返回一个新期约实例,这个新实例基于 onRejected 处理程序的返回值构建,它也通过
Promise.resovle()进行包装生成新期约,具体规则如下- 如果未提供 onRejected 处理程序,则
Promise.resovle()包装上一个期约兑现之后的值 - 如果没有显式返回语句,则
Promise.resovle()包装默认返回值undefined - 如果有显式返回语句,则包装这个值
- 抛出异常会返回被拒绝的期约(但返回错误值不会)
- 如果未提供 onRejected 处理程序,则
Promise.prototype.finally() —— 期约状态改变时执行
-
描述 也是
then()的语法糖,在期约状态改变时触发,可以避免 onFufilled 和 onRejected 程序中出现冗余代码,它无法知道期约的状态是兑现还是拒绝,因此主要用于清理代码 -
参数 接收一个函数参数,即 onFinally 处理程序
-
返回值 1.该方法被设计为一个与状态无关的方法,因此它大多表现为父期约的传递者 2.返回值为待定期约时,返回该待定期约 3.若 onFinally 处理程序抛出错误(显式抛出错误或返回拒绝期约),则返回拒绝期约
let p1 = Promise.resolve('foo');
// 1. 这里都会原样后传
let p2 = p1.finally();
let p3 = p1.finally(() => undefined);
let p4 = p1.finally(() => {});
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => 'bar');
let p7 = p1.finally(() => Promise.resolve('bar'));
let p8 = p1.finally(() => Error('qux'));
setTimeout(console.log, 0, p2); // Promise <fufilled>: foo
setTimeout(console.log, 0, p3); // Promise <fufilled>: foo
setTimeout(console.log, 0, p4); // Promise <fufilled>: foo
setTimeout(console.log, 0, p5); // Promise <fufilled>: foo
setTimeout(console.log, 0, p6); // Promise <fufilled>: foo
setTimeout(console.log, 0, p7); // Promise <fufilled>: foo
setTimeout(console.log, 0, p8); // Promise <fufilled>: foo
// 2. 返回值为待定期约
const p9 =p1.finnally(() => new Promise(() => {}))
console.asynclog(p9) // Promise <pedding>
// 3. 返回拒绝期约
const p10 = p1.finnally(() => { throw 'lcl' })
const p11 = p1.finnally(() => Promise.reject('lcl'))
console.asynclog(p10) // Promise <rejected>: lcl
console.asynclog(p11) // Promise <rejected>: lcl
[!tip] 注意
- 返回待定期约的情形不常见,因为只要期约被兑现或拒绝,新期约会传递对应情况的期约(兑现则传递父期约返回值,拒绝则传递拒绝期约)
- 如果为某一个Promise添加了多个处理程序,则同名程序会按照顺序执行(如 p1 为 promise,p1.then(); p1.then() 会按代码顺序执行)
传递兑现值和拒绝理由
- 描述
期约到了落定状态后,会提供其兑现的值或拒绝理由给相关状态的处理程序(即上面提到的处理程序),在执行器函数中,兑现值和拒绝理由作为
resovle()和reject()的第一个参数向后传递,这些值会被传递给它们各自的处理程序,作为 onFufilled 和 onRejected 处理程序的唯一参数
let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value) => console.log(value)); // foo
let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar
Promise.all() —— 所有期约兑现后兑现
[!tip] 注意
- 可迭代对象中的所有元素会通过
Promise.resovle()转换成期约
-
适用场景 需要等待所有请求完成后再统一处理结果的情况
-
描述 该方法接收一个可迭代对象作为参数,返回一个新期约,它会在传入的一组期约都兑现后才兑现,合成期约的兑现值是所有包含期约的兑现值组成的数组,按照迭代器顺序
如果有包含的的期约待定,则合成的期约待定
如果有包含的期约拒绝,则合成期约也拒绝,第一个被拒绝的期约会将自己的理由作为合成期约的拒绝理由,后续再拒绝的期约不会影响合成期约的拒绝理由,同时,合成期约会静默处理所有包含期约的拒绝操作
- 示例
Promise.all([1, Promise.resovle(), Promise.resovle(333)])
.then(res => console.log(res)) // [1, undefined, 333]
Promise.allSettled() —— 所有期约落定后兑现
-
适用场景 需要同时处理多个异步操作的结果,但是并不要求全部操作都成功
-
描述 该方法接收一个可迭代对象作为参数,可迭代对象中的所有元素都会通过
Promise.resolve()转换成期约,返回一个新期约,它会在所有包含期约落定后兑现
在所有期约落定后,合成期约兑现,其兑现值是一个对象的数组,包含可迭代对象中每个期约的输出,每个对象具有一个 status 属性,为 fufilled 或 rejected 表示对应期约的落定状态,根据 status 的属性值,还会有一个 value(兑现)或 reason(拒绝)属性
Promise.allSettled([1, Promise.reject('我拒绝')])
.then(res => console.log(res))
// [
// { status: fufiiled, value: 1 },
// { status: rejected, reason: '我拒绝' }
// ]
Promise.race() —— 落定第一个落定的期约
- 描述
该方法返回一个包装期约,是一组集合中最先兑现或拒绝的期约的镜像,接收一个可迭代对象,可迭代对象中的所有元素都会通过
Promise.resolve()转换成期约
如果第一个落定的期约是兑现的,则它的兑现值会成为合成期约的兑现值
如果第一个落定的期约是拒绝的,则它拒绝的理由会成为合成期约的拒绝理由
只要出现了第一个落定期约,那么合成期约的状态和传递的信息就会被固定,该方法只监控各个期约的运行,并不对期约行为进行控制,因此,当有一个期约胜出后,其他期约的运行并不会被停止,但它的处理结果将被忽略(兑现值或拒绝理由,又或者待定状态),即它不影响所有包含期约正常的处理程序,与 Promise.all() 类似,合成期约会静默处理所有包含期约的拒绝操作,并不会有错误被漏掉从而导致拒绝处理检查报错
Promise.race([Promise.reject(666), new Promise((resolve, reject) => setTimeout(resolve('已兑现')))])
.catch(err => consloe.log(err)) // 666
Promise.any() —— 兑现第一个兑现的期约
- 描述
该方法也不会影响所有包含期约的运行,与
Promise.race()不同的地方在于,它只会在第一个包含期约对现时兑现,兑现值为该包含期约的兑现值
如果所有包含期约都被拒绝,那么合成期约会被拒绝,拒绝理由是所有期约拒绝理由的 AggregateError 实例,可以通过捕获 AggregateError 并处理拒绝理由
const arr = [
Promise.reject('被拒绝了'),
Promise.reject('该怎么办'),
]
const p = Promise.any(arr)
p.catch(e => {
console.log(e) // AggregateError: All promises were rejected
console.log(e.errors) // ['被拒绝了', '该怎么办']
})
thenable 接口
[!tip] 注意
- 该 .then 方法并不是 Promise 意义上的 .then 处理程序,在处理 .then 方法时,会把它当作执行器函数(executor)执行,伪代码模拟放在 3. 中
- 描述
任何实现了
.then(resolve, reject)方法(实现了 thenable 接口)的对象就是 thenable ,会被 JS 当作 Promise 处理(可以将 thenable 当作野生 Promise)
处于对历史遗留和兼容性考虑,JS 设计了 thenable 接口,奉行鸭子类型(Duck Typing)策略,即只要你长的像鸭子,叫声像鸭子,那你就是鸭子
-
历史遗留与兼容性 在 ES6 标准化 Promise 之前(2015年之前),社区里已经有很多第三方 Promise 库了(比如 Bluebird, Q, jQuery.Deferred),为了让原生的 await 和 Promise.resolve() 能够无缝地与这些“旧时代的库”一起工作,ES6 制定了 Thenable 规则: 只要你能提供 then 方法,我就能跟你玩
-
自定义 thenable
// 示例
const obj = {
then(resovle, reject) {
resovle(666)
}
}
// 1. awat 实现了解包
async function fn() {
const a = await obj
console.log(a) // 666
}
fn()
// 2. Promise.resovle() 先解包,再包装
const p = Promise.resolve(obj)
console.asynclog(p) // Promise {<fulfilled>: 666}
⭐⭐处理 thenable(解包的过程)—— Promise.resolve() 和 await⭐⭐
- 相同点 它们都遵循 PromiseResolveThenableJob 规范:
- 检查 then 是否为函数
- 以该对象为 this 调用 then
- 传入两个“一次性”的原生回调函数
- 一旦其中一个回调被调用,Promise 状态就锁定了
- 不同点(微任务跳跃) 2.1 Promise.resolve(thenable)
- 创建一个 Promise
- 调度一个微任务 去执行 thenable.then
- 所以在当前宏任务中,Promise 是 Pending 的
2.2 await thenable
- 它本质上是 await Promise.resolve(thenable)
- 但因为 await 自身还会产生额外的微任务跳跃(用于恢复 async 函数上下文),所以在某些极端测试用例下(比如交替打印顺序),你会发现 await 比单纯的 Promise.resolve 慢那么一丢丢
结果:非原生的 Promise 被转换为原生 Promise
// 引擎内部真实逻辑模拟:Promise.resolve(thenable)
function resolveThenable(thenable) {
// 1. 创建新 Promise(同步)
// 此时 p 是 Pending 状态
// 我们拿到了它的控制权 internalResolve, internalReject
let internalResolve, internalReject;
const p = new Promise((res, rej) => {
internalResolve = res;
internalReject = rej;
});
// 2. 创建一个 Job(任务),准备去调用 then
// 这就是 PromiseResolveThenableJob
const job = () => {
try {
// 3. 在微任务里,才真正去调用 thenable.then
// 这就是为什么 Log 会先打印 Pending,因为这一步被推迟了
thenable.then(
(value) => internalResolve(value),
(reason) => internalReject(reason)
);
} catch (e) {
internalReject(e);
}
};
// 4. 将这个 Job 推入微任务队列(关键点!)
// 引擎并没有立即执行 job(),而是 enqueueJob(job)
queueMicrotask(job);
// 5. 立即返回那个还在 Pending 的 Promise p
return p;
}
- 总结 thenable 接口实现了 Promise 的互操作性
遇到 Promise.resolve(thenable) 时,解包它,再进行包装,会将它转化为原生 Promise ,状态随 thenable ,兑现值或拒绝理由跟 thenable 的相同
遇到 await thenable 时,会暂停并等待 then 方法调用 resolve
在写通用的异步工具库时,通常会先用 Promise.resolve(x) 包装一下参数,以确保无论是原生 Promise、Bluebird Promise 还是普通值,都能被统一处理
- 练习——每次看到这儿就练一下
console.log('--- 揭秘 Thenable 的转换过程 ---');
const myThenable = {
// 这里的 resolve 和 reject,就是引擎偷偷传进来的“遥控器”
// 它们连接着 Promise.resolve() 返回的那个新 Promise
then(resolveOfNewPromise, rejectOfNewPromise) {
console.log('1. [Thenable] 引擎调用了我的 then 方法');
console.log(' 参数1是函数吗?', typeof resolveOfNewPromise === 'function');
console.log(' 参数2是函数吗?', typeof rejectOfNewPromise === 'function');
// 我们来恶作剧一下:不立即调用它们,而是存起来
setTimeout(() => {
console.log('2. [Thenable] 1秒后,我决定调用第一个参数');
// 这一步,会触发那个原生 Promise 从 Pending 变为 Fulfilled
resolveOfNewPromise('我是被转化后的原生结果!');
}, 1000);
}
};
console.log('0. 开始转换...');
const p = Promise.resolve(myThenable);
console.log(' 此时 p 的状态是 Pending(等待 thenable 决定命运)');
p.then(val => {
console.log('3. [原生 Promise] 终于,我收到了结果:', val);
});
// 打印顺序为
// --- 揭秘 Thenable 的转换过程 ---
// 开始转换...
// 此时 p 的状态是 Pending(等待 thenable 决定命运)
// [Thenable] 引擎调用了我的 then 方法
// 参数1是函数吗? true
// 参数2是函数吗? true
// [Thenable] 1秒后,我决定调用第一个参数
// [原生 Promise] 终于,我收到了结果: 我是被转化后的原生结果!
JS 拒绝处理检查(微任务检查点)
- 发生时机 在微任务队列清空后,进行微任务检查
[!tip] 注意
- 由于执行器函数是同步的,因此在期约状态改变前使用 try/catch 是可以捕获到错误的
- 示例
try{
Promise.reject(new Error('this is a error'))
} catch(e) {
console.log(e)
}
Promise.reject(new Error('this is a error')).catch(e => console.log(e))
![[z_attachments/Pasted image 20251203133828.png]]
这是以上代码的打印结果,会发现,后面出现的 .catch 被先打印了,然后才抛出错误,而错误明明在前面,这是为什么呢?
涉及到事件循环的步骤,先进行代码顺序的讲解:
- 执行 try 发现第 2 行同步返回了一个被拒绝的期约(P1),但它没有拒绝处理程序(.catch()),JS 引擎会标记(P1)为未处理拒绝,放入一个内部的“可能未处理的拒绝列表(outstanding rejected promises list)
- catch 被跳过,因为 try 中没有抛出错误
- 执行第 6 行的程序,发现返回了一个被拒绝的期约(P2),它有错误处理程序(.catch()),推入微任务队列
- 同步代码执行完毕后,开始执行微任务(P2的错误处理程序),即打印错误(e => console.log(e)),在这个过程中,如果为 P1 添加拒绝处理程序,则会将他从拒绝列表中去除
- 在微任务队列清空后,JS 引擎立即开始执行微任务检查点,对于微任务过程中添加的所有
.catch(),会打包在一起,同步派发 rejectionhandled 事件,该事件用于全局错误追踪,同时浏览器会检查拒绝列表是否还有未认领的拒绝期约 ,如果还有,则遍历该列表,每次向 window 同步派发一个unhandleinjection事件,该事件的默认行为是,像控制台抛出错误,错误如上,在该事件中阻止默认行为则可以停止向控制台抛出错误
从上面的过程可以看出,“未处理拒绝”检查发生在微任务队列清空后,这是规范有意为之,这位动态添加拒绝处理程序提供了可能,从下面代码就能看出,但是我们最好能及时给出处理程序,以方便代码维护
let p
try{
p = Promise.reject(new Error('aaa'))
} catch(e) {
console.log(e)
}
Promise.reject(new Error('this is a error')).catch(e => {
console.log(e)
p.catch(e => console.log(e))
})
![[z_attachments/Pasted image 20251203141204.png]]
- 总结 总结执行顺序为: 执行同步代码 —> 执行微任务队列 —> 微任务队列清空后进入微任务检查点 —> 检查拒绝列表中是否有未认领拒绝期约 —> 如果有则同步派发 unhandleinjection 事件,向控制台抛出异常
异步函数
async 关键字
-
描述 async 关键字放在函数前面可以声明异步函数,可以使用在函数声明、函数表达式、箭头函数和方法中,没有添加 await 关键字的异步函数具有跟普通函数相同的行为(返回值除外)
-
返回值 异步函数的返回值会被隐式包装为一个期约。如果返回的是非期约值,相当于使用 Promise.resolve(value) 包装;如果返回的是期约,则新返回的期约状态将跟随该期约
⭐⭐在异步函数中抛出错误会返回拒绝的期约⭐⭐,整个异步函数内部如同被一个隐式的 try/catch 块包括一样,只要在异步函数执行上下文中抛出错误就会返回一个拒绝期约(setTimeout的回调等被退出宏任务的情况执行上下文已经改变,不能捕获它的错误)
async function fn() {
throw Error('this is a error')
}
fn().catch(e => console.log(e)) // Error: this is a error
- 注意 拒绝期约的错误并不会被异步函数捕获(用上面的话来说,拒绝期约并不会被 try/catch 捕获,因此不会被异步函数捕获)
async function fn() {
Promise.reject('婉拒了哈')
}
fn().catch(e => console.log(e)) // 控制台没有打印内容,未触发 catch 没有捕获到 reject 期约,由于 fn 内部的拒绝期约未提供拒绝处理程序,报错了
await 关键字
- 描述 由于异步函数主要针对不会马上完成的任务,需要暂停和恢复执行的能力,使用 await 可以暂停异步函数代码的执行
await 关键字会暂停执行异步函数后面的代码,让出 JS 运行时的执行线程,它会尝试解包对象的值,然后将值传递给表达式,再异步恢复异步函数执行
- ⭐⭐工作机制⭐⭐ 使用 await 关键字后,首先会对表达式进行求值,如果值不是期约,则使用 Promise.resolve(value) 包装为期约。完成包装后, JS 引擎会将当前异步函数的执行上下文(包括变量环境和执行指针)保存到堆内存中,并从调用栈弹出该函数,从而将控制权交还给事件循环(实现挂起)。引擎会为该期约注册一个内部的微任务回调(类似于 .then() ),当期约敲定(settled)时,该回调被推入微任务队列。待事件循环执行到该微任务时,引擎会从堆内存中恢复之前的执行上下文,将期约的兑现值传回 ,再进行解包,返回解包的结果(即前面方法的参数),程序继续向下执行
如果 await 作用于拒绝期约,await 表达式会先 在函数内部抛出异常 (异常内容为拒绝理由)。这个异常随即被 async 函数的隐式机制(try/catch)捕获,最终导致 async 函数返回的 那个新期约 转为拒绝状态,且拒绝理由相同
如果 await 表达式抛出了异常,同样会被 async 内部隐式机制捕获,异常值作为拒绝理由被写入函数返回的期约中
async function fn() {
await Promise.reject('拒绝了')
// 这后面的代码不会被执行了
console.log(5)
}
- 返回值
如果 await 关键字作用于一个实现了 thenable 接口的对象,那么这个对象可以由 await 来“解包”,如果不是,则这个值会被当作已经兑现的期约
// 作用于原始值,将原始值使用 Promise.resolve() 包装,再进行解包
async function fn() {
console.log(await 'lcl')
}
await 运行原理解释伪代码(仅供参考)
- 描述 表格仅供参考,具体还得看下面的代码
| 步骤 | 操作 | 底层机制 | 代码示例 |
|---|---|---|---|
| 1. 求值 | 同步计算 expression 的值 | 立即执行表达式,获取原始值 | await fn() → 先同步执行 fn() |
| 2. 包装 | 将值传递给 Promise.resolve() | 创建 Promise 包装: • Promise → 直接使用 • Thenable → 创建新 Promise • 基本类型 → 创建已解决的 Promise | Promise.resolve(expression) |
| 3. 挂起 | 暂停 async 函数执行 | 保存执行上下文(变量、指令指针) 控制权返回父函数/事件循环 | 协程挂起,返回 pending Promise |
| 4. 订阅 | 在 Promise 上注册回调 | 注册 onSuccess 和 onFail 回调 ,相当于注册了 then 方法对应 generator.next(val) 和 generator.throw(err) | .then(onFulfilled, onRejected) |
| 5. 恢复 | Promise settled 后唤醒 | 回调进入微任务队列 成功: await 表达式的值 = val 失败:在 await 位置抛出异常 | 微任务中恢复协程执行 |
// === Await 底层原理深度解析 ===
// --- 第一部分:文字详解 (The Theory) ---
/*
当 JS 引擎执行到 `await expression` 时,实际上进行了一次【协程挂起】。
具体流程如下:
1. **求值 (Evaluation)**:
引擎首先计算 `expression` 的值。例如 `await fn()`,先同步执行 `fn()` 拿到结果。
2. **包装 (Promisify)**:
引擎将这个值传递给 `Promise.resolve()` 进行包装。
- 如果是 Promise:直接使用。
- 如果是 Thenable:创建一个新 Promise,并安排微任务去展开它 (PromiseResolveThenableJob)。
- 如果是基本类型:创建一个已解决 (Resolved) 的 Promise。
3. **挂起 (Suspension)**:
async 函数在这个点**暂停执行** (Yield)。
引擎保存当前的堆栈信息(变量、指令指针),并将控制权交还给父函数或事件循环。
4. **订阅 (Subscription)**:
引擎在这个 Promise 上静默注册两个回调(相当于 .then(onSuccess, onFail)):
- **onSuccess**: 这里的逻辑是 `generator.next(val)`。
- **onFail**: 这里的逻辑是 `generator.throw(err)`。
5. **恢复 (Resumption)**:
当 Promise 状态变为 Settled (完成或失败) 时,对应的回调被推入【微任务队列】。
轮到微任务执行时,生成器被唤醒:
- 如果成功,`await` 表达式的值变为 `val`,代码继续向下。
- 如果失败,`await` 表达式的位置抛出异常 (throw),尝试进入 catch 块。
*/
// --- 第二部分:伪代码演示 (The Implementation) ---
// 这是一个手动实现的 "Async/Await 运行器"。
// 它展示了 JS 引擎是如何利用生成器 (Generator) 来实现 async 函数的。
function runAsync(generatorFunc) {
// 1. 调用生成器函数,获取迭代器 (Iterator)
const iterator = generatorFunc();
// async 函数总是返回一个 Promise
return new Promise((resolve, reject) => {
// 这是一个递归的“步进器”,负责驱动生成器
function step(key, arg) {
let result;
try {
// 关键操作:恢复生成器执行
// key 是 "next" (成功时) 或 "throw" (失败时)
// arg 是 Promise 的结果或错误信息
result = iterator[key](arg);
} catch (error) {
// 如果生成器内部抛出未捕获的错误,reject 整个 async 函数
return reject(error);
}
// 解构结果: { value, done }
const { value, done } = result;
if (done) {
// 如果生成器执行完毕 (return),解决外部 Promise
return resolve(value);
} else {
// 如果还没完,说明遇到了 await (即 yield)
// 核心魔法:无论 yield 出来什么,都用 Promise.resolve 包装
Promise.resolve(value).then(
// Promise 成功 -> 递归调用 step("next", val) 唤醒生成器
(val) => step("next", val),
// Promise 失败 -> 递归调用 step("throw", err) 让生成器内部报错
(err) => step("throw", err)
);
}
}
// 启动引擎!第一次 next() 不需要参数
step("next");
});
}
// --- 第三部分:实战验证 ---
// 这是一个 "伪装" 成 async 函数的 Generator
// 实际上:async function myFunc() { ... }
function* myAsyncFunction() {
console.log("1. [Async] 开始执行");
// 模拟: const res1 = await Promise.resolve(100);
const res1 = yield Promise.resolve(100);
console.log(`2. [Async] 拿到第一个 await 结果: ${res1}`);
try {
// 模拟: await Promise.reject("出错了");
yield Promise.reject("模拟网络错误");
} catch (e) {
console.log(`3. [Async] 捕获到异常: ${e}`);
}
// 模拟: const res2 = await "普通字符串";
const res2 = yield "普通字符串";
console.log(`4. [Async] 拿到第二个 await 结果: ${res2}`);
return "所有任务完成";
}
console.log("--- 启动 runAsync 模拟器 ---");
runAsync(myAsyncFunction).then(finalResult => {
console.log(`5. [Main] Async 函数最终返回值: "${finalResult}"`);
});
await 面试题
async function asy1() {
console.log(1)
await asy2()
console.log(2)
}
async function asy2() {
await setTimeout(() => {
Promise.resolve().then(() => {
console.log(3)
})
console.log(4)
}, 0)
}
const asyc3 = async () => {
Promise.resolve().then(() => {
console.log(6)
})
}
asy1()
console.log(7)
asyc3()
- 分析
首先运行
asy1(),进入函数体,打印 1 ,遇到await asy2(),需要等待 asy2() 的求值结果,进入 asy2() ,遇见await setTimeout...,需要等待计时器的求值结果,计时器同步返回一个计时器 id ,0秒后计时器回调被推入宏队列,由于 id 不是 Promise ,使用Promise.resolve(id)包装,包装后 Promise 为 fufilled 状态,满足条件,将 await 之后的代码推入微队列,可以看到 await 之后没有函数代码,因此将函数 asy2 的完成推入微队列(联想 yield,此刻在等待,即使后面没有代码也不能结束函数)
// 打印结果 —— 1 | // 宏队列 —— 计时器回调 | // 微队列 —— asy2 的完成
asy2 函数的完成被推入微队列后,由于 await asy2() 并没有返回求值结果,因此继续等待,JS主线程控制权跳出 asy1() ,执行同步代码,打印 7 ,目前打印结果为 1 7 ,随后执行 asy3() ,进入函数体,.then 方法将打印 6 推入微队列,同步代码结束
// 打印结果 —— 1 7 | // 宏队列 —— 计时器回调 | // 微队列 —— asy2 的完成, 打印6
从微队列取出 asy2 的完成,asy2完成,没有显示返回,因此隐式返回 undefiend ,返回值不是期约,因此被 Promise.resolve(undefined) 包裹,其实相当于返回了 Promise.resolve() ,此刻 asy1 中的 await asy2() 变成了 await Promise.resolve() ,由于期约兑现,将之后的内容推入微队列,此刻情况如下
// 打印结果 —— 1 7 | // 宏队列 —— 计时器回调 | // 微队列 —— 打印6, 打印2
推入微队列后,没有同步代码了,继续从微队列拿值,打印 6 和 2,此刻微任务队列清空,进入微任务检查点,不过它跟这题没有关系
// 打印结果 —— 1 7 6 2 | // 宏队列 —— 计时器回调 | // 微队列 ——
从宏任务中取出计时器回调,有一个 .then() ,将里面的打印 3 推入微队列,继续向下执行,遇到打印 4 的同步代码,打印 4
// 打印结果 —— 1 7 6 2 4 | // 宏队列 —— 计时器回调 | // 微队列 —— 打印3
从微任务中取出打印 3 的任务,打印,最后结果为 1 7 6 2 4 3
Iterator —— 迭代器
迭代器工厂函数、迭代器、可迭代对象的关系
可迭代对象实现了可迭代协议,具有 [Symbol.iterator] 属性,其属性值为迭代器工厂函数,每次调用该函数就会返回一个新的迭代器实例(一个一次性使用的对象),迭代器实例维护着一个指向关联可迭代对象的引用,迭代器的 next() 方法可以在可迭代对象中遍历数据
const arr = [3, 6]
// 存在 Symbol.iterator 属性
console.log(Symbol.iterator in arr) // true
// 该属性的值为迭代器工厂函数
console.log(arr[Symbol.iterator]) // ƒ values() { [native code] }
// 调用工厂函数返回一个迭代器实例
const iterator = arr[Symbol.iterator]()
console.log(iterator) // Array Iterator {}
// 对迭代器调用 next() 方法遍历数据
console.log(iterator.next()) // {value: 3, done: false}
console.log(iterator.next()) // {value: 6, done: false}
console.log(iterator.next()) // {value: undefined, done: true}
可迭代协议
- 可迭代对象 —— iterable 实现了正式的 Iterable 接口(可迭代协议),并且可以通过迭代器 Iterator 消费的对象,它们包含的元素是有限的,而且都具有无歧义的遍历顺序
常见的可迭代对象包括
- 数组
- 字符串
- Map
- Set
- arguments 对象
- Nodelist 等DOM集合类型(如 querySelectorAll 获得的类数组结构)
- 可迭代协议
即 Iterable 接口,实现可迭代协议要求对象需要暴露一个属性,属性键为
[Symbol.iterator],属性值为一个迭代器工厂函数,调用该工厂函数必须返回一个新迭代器
如果一个对象原型链上的父类实现了 Iterable 接口,那么这个对象也就实现了这个接口
- 判断是否为可迭代对象
const arr = []
const obj = {}
const str = 'aaa'
// 实现了迭代器工厂函数
console.log(arr[Symbol.iterator]) // ƒ values() { [native code] }
console.log(str[Symbol.iterator]) // ƒ values() { [native code] }
// 调用该工厂函数返回一个新的迭代器
console.log(arr[Symbol.iterator]()) // Array Iterator {}
console.log(str[Symbol.iterator]()) // String Iterator {}
// 未实现 迭代器工厂函数
console.log(obj[Symbol.iterator]) // undefined
迭代器协议
[!tip] 注意
- next 方法内部报错不会触发 return 方法,详情见下一节[[#return() —— 关闭迭代器钩子|关闭迭代器钩子]]
-
迭代器协议 迭代器协议就是 Iterator 接口,任何实现了 Iterator 接口的对象可以作为迭代器使用,该对象要求必须有一个
next()方法,具体如下 -
迭代器 迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象,迭代器 API 使用
next()方法在可迭代对象中遍历,该方法返回一个 IteratorResult 对象(迭代器结果对象)
| 属性 | 属性值 | 备注 |
|---|---|---|
| done | 布尔值,表示是否完成迭代 | 为 false 时,表示 next() 可以取得下一个值,完成迭代后为 true |
| value | 本次迭代值 | 完成迭代后为 undefined,即 done 为 true 时,value 为 undefined |
| 迭代器并不知道如何从可迭代对象中取得下一个值,也不知道可迭代对象有多大,只要迭代器达到 done: true 状态,后续调用 next() 就会一直返回相同的值 |
以下为一个迭代的示例,可以看到,迭代并不关心可迭代对象有多少元素,如下长度为3的数据,迭代三次后(第 6 行)返回的是 {value: 6, done: false} ,并没有收到停止迭代的通知,再次迭代后才获得迭代结束的 IteratorResult 对象
// 示例迭代
const arr = [3, 6, 9]
const iterator = arr[Symbol.iterator]()
console.log(iterator.next()) // {value: 3, done: false}
console.log(iterator.next()) // {value: 6, done: false}
console.log(iterator.next()) // {value: 6, done: false}
console.log(iterator.next()) // {value: undefined, done: true}
//迭代器达到 done: true 状态,后续调用 next() 就会一直返回相同的值
console.log(iterator.next()) // {value: undefined, done: true}
console.log(iterator.next()) // {value: undefined, done: true}
- 注意 迭代器并不与对象某个时刻的快照绑定,这意味着如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化
迭代器维持着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象
自定义可迭代对象
-
描述 任何实现了可迭代协议的对象都是可迭代对象,下面通过闭包保存了计数器变量,使每一次调用工厂函数生成的迭代器互不影响
-
实例只能创建一个迭代器 如果由实例自身维护状态,next() 方法挂在实例上,而
[Symbol.iterator]方法为[Symbol.iterator]() { return this }时,实例既是可迭代对象,又是迭代器,它只能迭代一次
它适合应用于 数据流(Stream)、生成器函数(Generator)产生的对象 等场景,像一瓶水一样,喝完就没有了
// 实例只能创建一个迭代器,这句话的含义是所有迭代器共享状态,共享进度,迭代完就没有了
class CounterSingle {
constructor(limit) {
this.limit = limit;
this.count = 1; // 状态保存在实例属性中
}
// 1. 必须实现 next() 方法,成为 Iterator
next() {
if (this.count <= this.limit) {
return { value: this.count++, done: false };
} else {
return { value: undefined, done: true };
}
}
- 实例可以创建多个独立迭代器
如果实例自身不维护迭代状态,而是由迭代器自身维护,那么一个实例可以创建多个独立的迭代器,如果迭代器还具有
[Symbol.iterator]() { return this },那么它不仅是迭代器,还是一个可迭代对象
这种设计结构应用于 数组、Map、Set、字符串。你可以对同一个数组遍历无数次,互不影响
// 1. 实例为可以创建多个独立迭代器的可迭代对象,设置了停止方法
class Counter {
constructor(limit) {
this.limit = limit
}
[Symbol.iterator]() {
let count = 1
let limit = this.limit
return {
next() {
if (count <= limit) {
return {
value: count++,
done: false
}
} else {
return {
value: undefined,
done: true
}
}
},
return() {
console.log('提前终止迭代')
return { done: true }
}
}
}
}
const counter = new Counter(3)
for (const item of counter) {
console.log(item)
}
// 1
// 2
// 3
// 2. 实例可以创建多个迭代器的可迭代对象,且每个迭代器也是可迭代对象
class CounterMulti {
constructor(limit) {
this.limit = limit;
}
// 实例本身没有 next() 方法,它不是迭代器,只是可迭代对象
[Symbol.iterator]() {
let count = 1; // 状态保存在闭包里,每次调用都是新的
let limit = this.limit;
// 返回一个新的迭代器对象
return {
// ⭐⭐让返回的迭代器本身也是可迭代的(最佳实践)⭐⭐
[Symbol.iterator]() { return this; },
next() {
if (count <= limit) {
return { value: count++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
}
return() —— 关闭迭代器钩子
[!tip] 注意
- 并非调用 return 方法才终止了迭代,终止迭代是引擎的行为,因此,return 方法只是给我提供了一个迭代终止时的钩子,让我们可以插入自己的逻辑,如清理文件等
- 规范建议(并非强制性)将 return() 的第一个参数作为 IteratorResult 对象的 value 值,但现实情况中,消费方大多不会传递 return() 参数,所以绝大多数迭代器的 return() 表现为返回 { done: true }
- 迭代器内部报错,如 next 内部 throw 抛出错误,并不会调用 return 方法,因为这意味着迭代器已经崩溃了,引擎会直接抛出异常并关闭迭代器
- 描述
return()方法是可选的,用于指定在迭代器提前关闭时执行的逻辑,它能关闭迭代器(使用它返回表示迭代结束的 IteratorResult 对象,表示迭代结束),在提前终止迭代时,JS引擎会调用这个方法,在这个钩子中,可以关闭文件句柄、关闭数据库连接等
常见的提前终止迭代的情况有
- for...of 循环通过 break , countinue , return 或 throw
- 解构操作并未消费所有值
-
返回值
return()方法必须返回一个有效的 IteratorResult 对象,且该对象的 done 必须为 true,可以只返回 **`{ done: true } -
注意 内置的语言结构(如 break 等)在发现还有更多值可以迭代,但不会消费这些值时,会自动调用
return()方法
// 使用上面定义的自定义可迭代对象
const counter = new Counter(5)
// 1. break 终止
for (const i of counter) {
if (i > 2) {
break
}
console.log(i)
}
// 1
// 2
// '提前终止迭代'
// 2. throw 终止
try {
for (const i of counter) {
if (i > 2) {
throw 'error'
}
console.log(i)
}
} catch (e) {}
// 1
// 2
// '提前终止迭代'
// 3. 数组解构终止
const [a, b] = counter
// '提前终止迭代'
console.log(a, b) // 1, 2
对于同一个迭代器实例而言,如果迭代器没有关闭,则可以继续从上次离开的地方进行迭代,因为每个迭代器实例维护独立的状态
const arr = [1, 2, 3, 4, 5]
let iterator = arr[Symbol.iterator]()
for (const i of iterator) {
console.log(i)
if (i > 2) break
}
// 1
// 2
// 3
for (const i of iterator) {
console.log(i)
}
// 4
// 5
判断迭代器是否可关闭
- 方法 测试其 return 属性是否为函数对象,对于没有 return() 方法的迭代器,仅仅给它增加一个同名方法是不行的,还要实现对应的关闭条件(返回 IteratorResult 对象,且 { done: true }
// 从以下示例可以看出 数组没有 return 方法,
const arr = []
const iterator = arr[Symbol.iterator]()
console.log(typeof iterator.return) // undefined
Generator —— 生成器
- 描述 生成器是一个极为灵活的结构,拥有在一个函数快内暂停和恢复代码执行的能力
生成器(函数)
- 描述
生成器的形式是一个函数,在函数名称前面加上一个星号(
*),表示它是一个生成器,所有能定义函数的地方都能定义生成器,箭头函数不能用来定义生成器 ,表示生成器函数的型号不受两侧空格影响
// 1. 生成器函数声明
function* fn() {}
// 2. 生成器函数表达式
let fn = function* () {}
// 3. 作为对象字面量方法的生成器函数
const obj = { *fn() {} }
class Lcl {
// 4. 作为类实例方法的生成器函数
*fn() {}
// 5. 作为类静态方法的生成器函数
static *abc() {}
}
// 6. 标识生成器函数的星号不受两侧空格影响,以下三种都是有效的写法
function* fn() {}
function * fn() {}
function *fn() {}
生成器对象
- 描述 调用生成器函数会返回一个生成器对象,生成器对象一开始处于暂停执行(suspended)的状态
生成器对象实现了 Iterator 接口(迭代器协议),因此它也具有 next() 方法,调用该方法会让生成器开始或恢复执行,next() 方法返回一个 IteratorResult 对象,空生成器函数调用一次 .next() 方法就会返回 { done: true, value: undefined }
[!tip] 注意
- 由于 next() 只会在遇到 yield 或 return 时才会停止,而它通过该方法调用时,返回值
| 属性 | 含义 |
|---|---|
| done | 迭代状态,true 表示结束 |
| value | 生成器函数的返回值,没有为 undefined |
生成器对象还实现了 Iterable 接口(可迭代协议),它是可迭代对象,它的默认迭代器指向自身([Symbol.iterator]() { return this })
function * fn() {
console.log(111)
return 333
}
// 1. 获取生成器对象,初次调用生成器函数不会运行函数内的代码
const generator = fn()
console.log(generator) // fn {<suspended>}
// 2. 具有 next() 方法
console.log(generator.next) // ƒ next() { [native code] }
// 3. 具有 [Symbol.iterator] 属性,且其默认迭代器指向自身
console.log(generator[Symbol.iterator]() === generator) // true
// 4. 调用 next() 方法恢复运行
const res = generator.next() // 执行函数,打印 111
console.log(res) // {value: 333, done: true}
yield
- 描述
可以让生成器停止和开始运行,生成器函数在遇到 yield 之前会正常执行,遇到该关键字后会停止执行,函数作用域的状态被保留,停止执行的生成器函数只能通过在生成器对象上调用
next()方法来恢复执行
yield 的行为有点像函数中的 return ,可以作为函数的中间返回语句,它生成的值会出现在 next() 方法返回的对象中,通过 yield 退出的生成器函数会处于 done: false 状态,通过 return 退出的生成器函数处于 done: true 状态
生成器内部执行会针对每个生成器对象区分作用域,在一个生成器对象上调用 next() 不会影响其他生成器对象
yield 只能直接在生成其函数内部使用,在其他函数或嵌套的非生成器函数中使用会抛出语法错误
function* fn() {
yield 1
yield '字符串'
return 9
}
const generatorObject = fn()
console.log(generatorObject.next()) // { done: false, value: 1 }
console.log(generatorObject.next()) // { done: false, value: '字符串' }
// return 也被捕获了,但是通过 return 退出的,done 为 true
console.log(generatorObject.next()) // { done: true, value: 9 }
// 再次调用,变为 undefined
console.log(generatorObject.next()) // { done: true, value: undefined }
对生成器对象迭代
- 描述
对生成器对象显式调用
next()方法显然很不健康,由于它同时还是可迭代对象,可以对他迭代
function* fn() {
yield 1
yield 2
yield 9
}
const generatorObj = fn()
for (const i of generatorObj) {
console.log(i)
}
// 1
// 2
// 9
使用 yield 实现输入和输出
[!tip] 注意
- 简单来说,在暂停期间,
next()return()throw()方法提供的第一个参数都会被暂停的 yield 接收- 而 yield* 接收可迭代对象关闭时的 value 值,具体来说就是 done 的值第一次为 true 时的 value值
- 描述
除了作为函数的中间返回语句外,它还可以作为函数的中间参数使用,在迭代输出时,可以理解为 return,yield 后面的值会输出,在下一次调用 next 方法时,yield 会接收到
next()的第一个参数(基于这个特点,第一次调用next()方法时传入的值不会被使用)
function* fn(param) {
console.log(param)
console.log(yield)
console.log(yield)
}
// 注意这里传入了参数
const go = fn('aaa')
// 发现参数 '打印我'被忽略了,因为它是第一个 next ,只有使用了 某个 yield 暂停后,再对 next 传入参数,才会被该 yield 接收
// 函数执行到第一个 yield 后暂停
go.next('打印我') // aaa
// 开启执行,传入参数给第一个 yield ,执行到第二个 yield 时停止
go.next('这次会打印') // 这次会打印
// 传入参数给第二个 yield 函数执行完毕,这次返回 { value: undefined, done: true }
go.next('这次也会打印') // 这次也会打印
- 结合迭代生成器 结合前面的对生成器对象迭代操作,可以方便的实现很多需求,例如生成索引
function* fn(time) {
for (let i = 0; i < time; i++) {
yield i
}
}
for (const i of fn(5)) {
console.log(i)
}
// 0
// 1
// 2
// 3
// 4
yield*
- 描述
可以使用
*增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值,星号两侧的空格不影响其行为
下面展示了两个行为上等价的代码结构,但是,yield* 实际上并不会创建一个生成器函数,并进行迭代,也不会像 for..of 一样,它更复杂,因为它需要传递外界的方法,如 next() return() throw()
function* fn() {
for (const x of [1, 2, 3]) {
yield x
}
}
for (const i of fn()) {
console.log(i)
}
// 1
// 2
// 3
// 上面的代码等价于
function* fn() {
yield* [1, 2, 3]
}
for (const i of fn()) {
console.log(i)
}
// 1
// 2
// 3
// 使用多个可迭代结构
function* fn() {
yield* [1, 2, 3]
yield* [6, 9, 12]
}
for (const i of fn()) {
console.log(i)
}
// 1 2 3 6 9 12(为方便把多行打印合在一行写了)
⭐yield* 的值⭐
- 描述 yield 的值是与其关联的迭代器返回 done: true 时的 value 属性*
对于普通迭代器来说(迭代器没有被提前关闭,或若被提前关闭但 return() 返回 { done:true, value: undefined } 或 { done: true } ),这个值是 undefined
function* fn() {
console.log('iteraValue', yield* [1])
}
for (const x of fn()) {
console.log(x)
}
// 1
// iteraValue, undefined
对于生成器产生的迭代器来说,这个值是生成器函数的返回值
function* generator() {
return 666
}
function* fn() {
console.log('iteraValue', yield* generator())
}
for (const x of fn()) {
console.log(x)
}
// 第一次迭代 generator() ,就得到了 { done: true, value: 666 },导致循环体不运行,直接结束了
// iteraValue, 666
使用 yield* 实现递归
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1)
yield n - 1
}
}
for (const x of nTimes(3)) {
console.log(x)
}
// 0
// 1
// 2
生成器作为默认迭代器
- 描述 生成器对象实现了可迭代协议,又实现了迭代器协议,而且使用 yield 能自动返回 IteratorResult 对象,因此它特别适合作为自定义可迭代对象的默认迭代器
例如以下类,使用生成器作为默认迭代器,它的实例每一次 for...of 迭代时,都会调用 [Symbol.iterator] 方法,而这个方法每次调用返回一个新的生成器对象,生成器对象能满足所有迭代器的要求,并且比手写迭代器相比,它不需要考虑边界条件,不需要手动返回 IteratorResult 对象
// 使用生成器作为迭代器
class Lcl {
constructor() {
this.value = [1, 2, 3]
}
*[Symbol.iterator]() {
yield* this.value
}
}
const lcl = new Lcl()
for (const x of lcl) {
console.log(x)
}
for (const x of lcl) {
console.log(x)
}
// 手写可迭代协议,又臭又长啊。。。
class Lcl {
constructor() {
this.value = [1, 2, 3]
}
[Symbol.iterator]() {
let index = 0
const value = this.value
return {
next() {
if (index < value.length) {
return {
value: value[index++],
done: false
}
} else {
return {
value: undefined,
done: true
}
}
}
}
}
}
提前关闭生成器
- 描述
与迭代器类似,生成器也有一个的
return()方法用于提前关闭生成器(因为生成器对象其实也是迭代器,并且它同时还是可迭代对象),不同的是,对于生成器来说,return()方法是必须的,除了 return() 方法外,生成器还特有throw()方法
function* fn() {
}
console.log(fn().next) // ƒ next() { [native code] }
console.log(fn().return) // ƒ return() { [native code] }
console.log(fn().throw) // ƒ throw() { [native code] }
return() —— 强制生成器进入关闭状态
[!tip] 注意
- 生成器的
return()要求传入的第一个参数为 IteratorResult 对象的 value 值
- 描述
return()方法提供的值就是最终迭代器对象的值,关闭生成器后,无法恢复,后续再次调用next(param)方法都会显示 done: true,而提供的返回值也不会被存储或传播,即永远返回{ done: true, value: undefiend }
function* generator() {
yield* [1, 2, 3]
}
const g = generator()
console.log(g) // generator {<suspended>}
console.log(g.return(9)) // {value: 9, done: true}
// 生成器被关闭了
console.log(g) // generator {<closed>}
// 后续再次调用 next() 方法都会显示 done: true,而提供的返回值也不会被存储或传播
console.log(g.next(666)) // {value: undefined, done: true}
throw() —— 注入错误到生成器对象中
[!tip] 注意
- 如果
thorw(err)方法注入了错误,生成器却没有处理,那么整个生成器会直接报错,而throw(err)这一行代码作为报错源会被浏览器打印,错误为 "Uncaught err",简单来说,如果生成器内部处理了错误,那么生成器函数将继续运行,直到遇到 yield 或函数返回,如果生成器内部没有捕获错误,会导致throw(err)抛出异常
-
描述
throw()就像一个特殊的next()方法,它在抛出错误的同时也恢复了生成器函数的运行,在暂停时将一个错误对象注入到生成器对象中,如果错误未能在生成器内部处理,生成器将会关闭,如果处理了错误,代码会继续运行(可以简单把它当作一个抛出错误的函数) -
返回值 它会恢复生成器运行,如果错误被捕获则继续运行,知道遇见下一个 yield 或 return ,将它们抛出的值作为 value 放入返回值中,返回值是 IteratorResult 对象(其实它的返回值跟 next() 方法相同,返回逻辑跟这个方法也相同)
// 1. 在外部处理错误,生成器关闭
function* fn() {
yield* [1, 2, 3]
}
const g = fn()
console.log(g) // fn {<suspended>}
try {
console.log(g.throw('注入错误')) // 由于生成器内部未处理错误,导致 g.throw() 抛出错误未能执行到 console.log(...),所以这一步没有任何打印
} catch(e) {
console.log(e) // 注入错误
}
console.log(g) // fn {<closed>}
// 2. 在内部处理错误
function* fn() {
try {
yield 1
yield 2
yield 3
} catch (e) {
console.log(e)
}
yield 4
}
const g = fn()
console.log(g)
console.log(g.next())
console.log(g.throw('this is an error'))
console.log(g.next())
console.log(g)
// 步骤阐释:第一次调用 g.next() 生成器函数运行,遇到 yield 1 ,暂停运行,抛出 1,然后紧接着调用 go.throw('错误'),yield 1 中的 yield 抛出一个错误,错误的详细信息为(this is an error),错误被 try/catch 捕获,跳转到 catch 块中执行,执行完之后遇到 yield 4 ,代码暂停执行,抛出 4 ,之后再执行 g.next() ,yield 4 中的 yield 被赋值为 undefinded ,代码继续运行,发现函数结束,返回值为 undefined,所以打印 { value: undefined, done: true },于是有以下打印结果
// fn {<suspended>}
// {value: 1, done: false}
// this is an error 这一行还有下一行都是 go.throw 导致的
// {value: 4, done: false}
// fn {<suspended>}
throw() 面对 yield* interable 的标准化规范流程
-
委托建立 (Delegation) 当执行 yield* iterable 时,生成器调用
iterable[Symbol.iterator]()获取 内层迭代器 ,并将后续的 next() / throw() / return() 操作全权委托给它 -
异常传播 (Error Propagation) 当外部调用
generator.throw(err)时,引擎尝试调用 内层迭代器 的.throw(err)方法 -
默认行为 (Default Behavior) 如果 内层迭代器 不支持 .throw (例如数组迭代器) 清理 : 引擎自动调用内层迭代器的 .return() 方法(如果存在),以关闭内层迭代器 抛错 : 引擎在 yield* 表达式的当前位置抛出异常
-
流程中断(Interruption) 该异常导致 yield* 循环 立即中止 。控制流跳转至最近的 catch 块。因此,内层迭代器剩余的元素被跳过
// 验证代码
console.log("--- 验证:内层迭代器确实被关闭了 ---");
// 我们造一个可以监控自己是否被关闭的迭代器
const spyIterator = {
[Symbol.iterator]() { return this; },
next() {
console.log(" [Inner] next() 被调用");
return { value: 1, done: false };
},
// 数组迭代器没有 throw,我们这里故意也不写 throw
// 但是我们写了 return,看看它会不会被调用
return() {
console.log(" [Inner] return() 被自动调用了!(内层已关闭)");
return { done: true };
}
};
function* outer() {
try {
yield* spyIterator;
} catch (e) {
console.log(`[Outer] 捕获异常: ${e}`);
console.log("[Outer] 虽然 yield* 中断了,但我还可以继续运行!");
}
yield "Outer 最后的倔强";
}
const g = outer();
console.log("1. g.next() ->", g.next());
console.log("2. g.throw('炸弹') ->");
// 观察控制台:
// 1. Inner 的 return() 会被调用(证明内层死了)
// 2. Outer 的 catch 会执行(证明异常抛出了)
// 3. Outer 会继续 yield 最后一个值(证明外层没死,只是那行 yield* 死了)
console.log(g.throw('炸弹'));
// 控制台打印结果
// --- 验证:内层迭代器确实被关闭了 ---
// [Inner] next() 被调用
// 1. g.next() -> {value: 1, done: false}
// 2. g.throw('炸弹') ->
// [Inner] return() 被自动调用了!(内层已关闭)
// [Outer] 捕获异常: TypeError: The iterator does not provide a 'throw' method.
// [Outer] 虽然 yield* 中断了,但我还可以继续运行!
// {value: 'Outer 最后的倔强', done: false}
异步生成器
- 异步可迭代协议 实现了 [Symbol.asyncIterator] 方法的对象
该方法返回一个异步迭代器
通常使用 async function* 来创建
- 异步迭代器协议 具有 next 方法,跟同步迭代器协议不同的是,它要求 next 方法返回一个 promise
如果兑现,则兑现值为 { done, value } 的 IteratorResult 对象
如果拒绝则返回拒绝的Promise(直接返回这个被拒绝的Promise),拒绝理由并不要求是 IteratorResult 对象,但是一旦返回拒绝期约,异步迭代器将被关闭
-
异步迭代器与异步可迭代对象 实现了异步迭代器协议的是异步迭代器,异步可迭代对象同理
-
异步生成器 异步生成器由
async function*定义
异步生成器函数不是普通的异步函数,其返回值为异步生成器,它既是异步可迭代对象又是异步迭代器
如果不是 promise 会被 Promise.resolve() 包装,如果 yield 后面跟的是 rejected promise,会关闭异步迭代器,结束迭代,如果没有 .catch 这个拒绝期约会报错
同步迭代器的 next 方法是同步的,会立即恢复生成器函数执行,而 异步生成器创建的迭代器 ,其 next 方法的执行机制是异步的,调用它会将生成器函数恢复执行的操作推入微队列
理由是,如果 next 方法是 同步 执行生成器代码的,那么生成器内部的同步代码(如 console.log )就会同步执行,这破坏了 async 上下文中执行顺序的一致性(Run-to-completion)
async function* gen() {
console.log('A');
yield 1;
console.log('B');
}
gen().next()
- ⭐⭐重点⭐⭐
调用 next 方法时同步返回一个占位 Promise(outerP),同时将生成器恢复执行的操作推入微队列,生成器恢复执行后,执行到
yield p时,再次暂停,并把p抛出,但抛出的 p 并不满足 IteratorResult 对象的要求,因此引擎会使用如下伪代码逻辑处理,关联 outerP 与 p 的状态,outerP 只有在 p 落定(settle)后,才会resolve/reject,再次之前是 pendding
// 引擎内部逻辑
Promise.resolve(p).then(
// 如果 p 成功了
(value) => {
// 这里的 resolveOuterP 就是 outerP 的 resolve 函数
resolveOuterP({ value: value, done: false });
},
// 如果 p 失败了
(err) => {
// 这里的 rejectOuterP 就是 outerP 的 reject 函数
rejectOuterP(err);
}
);
- 对比表
| 特性 | 同步生成器 ( function* ) | 异步生成器 ( async function* ) |
|---|---|---|
| next() 返回值 | { value, done } | Promise<{ value, done }> |
| 内部代码执行 | 同步执行 (阻塞) | 异步执行 (微任务) |
| yield await | 不支持 await | 支持 await |
| 消费方式 | for...of | for await...of (推荐) |
for await...of —— 消费异步数据
- 语法
for await (const variable of iterable) {
}
- 执行机制
一、获取迭代器(Get Iterator)。首先尝试获取
[Symbol.asyncIterator]如果没有则尝试获取[Symbol.iterator],再没有就报错TypeError: iterable is not iterable
二、循环调用(Loop & Next)。对 next 的返回值全部使用 Promise.resolve() 包裹(以应对上一步获取到同步迭代器的情况)
三、等待结果 (Await Result)。等待上一轮循环的 Promise 落定,将 Promise 解析为 { value, done }
四、判断与执行 (Check & Execute)。如果 done 为 true 退出循环,如果 done 为 false ,将 value 赋值给变量 variable(如上语法节所示),执行循环体,完成后回到第二步
-
适配同步可迭代对象 可以使用该语法迭代同步可迭代对象,但这样有个问题,同步的结果在 第三步(Await Result) 中使用了 await 来处理,产生了一次微任务时间,因此使用它迭代同步可迭代对象会比 for...of 慢
-
错误处理 只要在循环过程中报错(next 方法中报错或者循环体报错),都会被引擎捕获:
一、关闭迭代器。在循环体中报错,尝试调用 return 方法,进行资源清理,并强制关闭迭代器,在 next 方法中报错(同步或异步报错),不会调用 return 方法(因为迭代器报错,迭代器以崩),而是引擎直接关闭迭代器
二、完成之后将错误抛出,让外部的 try/catch 来处理
queueMicrotask
- 描述 queueMicrotask 会将其回调同步推入微任务队列末尾,可以方便创建微任务
queueMicrotask(callback)
// 行为上等价于
Promise.resolve().then(callback)
判断类型
- 方法
function getType(target) {
const originType = Object.prototype.toString.call(target)
const type = originType.slice(8, -1).toLowerCase()
return type
}
- 原理
ES6 新增了一个
[Symbol.toStringTag]属性,它的值是对象的字符串描述符,可以自定义对象的类型,每个对象都有 toString 方法,如数组的 toString 方法会拼接字符串,数字的 toString 方法会把数组转换为对应字符串,而Object.prototype.toString()的作用是返回当前对象的字符串描述符(即内部类型标签),而call()会修改this的指向,可以通过这个方法将toString()的this修改为我们需要检测的目标,从而返回它的内部属性标签
而由于内部标签的格式一致,可以通过分割索引 8 到倒数第一个字符串,来获得其属性字符串,如 Number ,再转小写可以获得精确的 type,如果自定义类型设置了 [Symbol.toStringTag] 属性,甚至能获得自定义对象的类型
// 内部属性标签,都为以下形式
[object Number]
[object Array]
- 修改自定义对象类型
const my = {
a: 1,
[Symbol.toStringTag]: '66666666666666'
}
console.log(Object.prototype.toString.call(my)) // [object 66666666666666]
位运算
| 运算符 | 名称 | 核心逻辑(通俗版) | JS/TS 示例(代码 + 结果) | 典型应用场景 |
|---|---|---|---|---|
| | 按位或 | 对应二进制位有 1 则为 1(组合) | 1 | 2 → 3 (0001 | 0010 = 0011) 3 | 4 → 7 (0011 | 0100 = 0111) | 组合权限/状态(如读 + 写权限) |
& | 按位与 | 对应二进制位全 1 才为 1(判断) | 3 & 1 → 1 (0011 & 0001 = 0001) 5 & 1 → 1 (0101 & 0001 = 0001) | 判断是否包含某状态、奇偶判断 |
^ | 按位异或 | 对应二进制位不同则为 1(切换) | 3 ^ 1 → 2 (0011 ^ 0001 = 0010) a=1,b=2; a^=b; b^=a; a^=b → a=2,b=1 | 切换状态(有则删/无则加)、交换变量 |
~ | 按位非 | 逐位取反(等价于 -(n+1)) | ~2 → -3 (~00000010 = 11111101 → -3) ~0 → -1 | 配合 & 移除状态(如 perm & ~Write) |
<< | 左位移 | 二进制左移 n 位,补 0(×2ⁿ) | 1 << 2 → 4 (1×2²) 2 << 3 → 16 (2×2³) | 生成 2 的幂(位掩码基础) |
>> | 有符号右位移 | 右移 n 位,保留符号位(÷2ⁿ 取整) | 6 >> 1 → 3 (0110 >>1 = 0011) -6 >> 1 → -3 | 快速整除 2(比 /2 更高效) |
>>> | 无符号右位移 | 右移 n 位,补 0(负数变正数) | -1 >>> 0 → 4294967295 -8 >>> 1 → 2147483644 | 处理 32 位无符号整数、底层二进制操作 |
⭐深入 DOM⭐
Node 类型
nodeType —— 节点类型
每个节点都有 nodeType 属性,表示该节点的类型,共有 12 中 nodeType ,浏览器并未完全支持,12 种类型如下,其中最常用的是元素节点和文本节点
| 类型代码 (常量) | 数字值 | 含义描述 |
|---|---|---|
Node.ELEMENT_NODE | 1 | 元素节点(如 <div>、<p>) |
Node.ATTRIBUTE_NODE | 2 | 属性节点(已过时,推荐使用 Element.attributes) |
Node.TEXT_NODE | 3 | 文本节点(元素或属性中的文本内容) |
Node.CDATA_SECTION_NODE | 4 | CDATA 节点(仅限 XML) |
Node.ENTITY_REFERENCE_NODE | 5 | 实体引用节点(已过时,仅限 XML) |
Node.ENTITY_NODE | 6 | 实体节点(已过时,仅限 XML) |
Node.PROCESSING_INSTRUCTION_NODE | 7 | 处理指令节点(仅限 XML) |
Node.COMMENT_NODE | 8 | 注释节点(<!-- 注释 -->) |
Node.DOCUMENT_NODE | 9 | 文档节点(整个文档,document 对象) |
Node.DOCUMENT_TYPE_NODE | 10 | 文档类型节点(<!DOCTYPE html>) |
Node.DOCUMENT_FRAGMENT_NODE | 11 | 文档片段节点(轻量级虚拟容器) |
Node.NOTATION_NODE | 12 | 符号节点(已过时,仅限 XML) |
// 示例
const h1 = document.querySelector('h1')
console.log(h1.nodeType === Node.ELEMENT_NODE, h1.nodeType === 1) // true true
nodeName 与 nodeValue
节点名称与节点值,完全取决于节点类型(nodeType),以元素节点为例,nodeName 等于元素标签名,nodeValue 为 null
const h1 = document.querySelector('h1')
console.log(h1.nodeName, h1.nodeValue) // H1 null
⭐节点关系
- 父节点、子节点、同胞节点
<!-- div 为 两个 span 的父节点, span 为 div 的子节点, span 之间互为同胞节点 -->
<div>
<span></span>
<span></span>
</div>
childNodes —— 子节点类数组对象(可迭代对象)
[!tip] childNodes 属性包含所有类型的子节点(即上述提到的 12 种类型),因此它不太干净
每个节点都有 childNodes 属性,包含一个 NodeList 的实例,是一个类数组对象,可迭代,NodeList 独特的地方在于,它是一个对 DOM 结构的查询,因此 DOM 结构的变化会实时在 NodeList 中反映出来,因此我们通常说 NodeList 是实时活动对象,而非某一时刻的内容快照
除了像访问数组一样获取 childNodes 的值之外,还可以使用 item 方法
const div = document.querySelector('div')
console.log(div.childNodes[0] === div.chilNodes.item(0))
使用解构赋值、slice 方法或者 Array.from() 方法可以将 nodeList 对象转换为数组
const div = document.querySelector('div')
console.log(div.childNodes)
// 下面三者在内容上是完全一致的,但由于引用不同,比较会为 false
const [...arr] = div.childNodes
const arr2 = Array.prototype.slice.call(div.childNodes, 0)
const arr3 = Array.from(div.childNodes)
console.log(arr === arr2, arr === arr3, arr2 === arr3) // false false false
⭐childNodes 注意事项
空格、换行符都算作文本节点,相邻的文本节点会被合并,如下
<body>
<div>
<span></span>
</div>
</body>
<script>
const div = document.querySelector('div')
console.log(div.childNodes) // 3 个节点,text span text
</script>
<body>
<div><span></span></div>
</body>
<script>
const div = document.querySelector('div')
console.log(div.childNodes) // 1 个节点,span
</script>
hasChildNodes() —— 是否有子节点
对节点调用 hasChildNodes() 方法可以知道它是否包含一个或多个子节点,返回一个布尔值,这比通过 nodeLiist.length 属性要方便得多
// 示例
<div>
<span></span>
<span></span>
</div>
<script>
const div = document.querySelector('div')
const span = document.querySelector('span')
console.log(span.hasChildNodes()) // false
console.log(div.hasChildNodes()) // true
<script>
parentNode —— 指向父元素
每个节点都有 parentNode 属性,指向其父元素
在 childNodes 中的所有元素都有共同的 parentNode
<div>
<span></span>
<span></span>
</div>
<script>
const span = document.querySelector('span')
consol.log(span.parentNode) // div 元素
</script>
previousSibling 和 nextSibling —— 同胞节点的前一个/后一个节点
[!tip] 这里的节点包括 12 种节点类型,因此它也不太干净
每个节点都有这两个属性,分别指向前一个/后一个节点,第一个节点的 previousSibling 为 null ,最后一个节点的 nextSibling 为 null
<div>
<span></span>
<span></span>
</div>
<script>
const span = document.querySelector('span')
console.log(span.previousSibling, span.nextSibling) // 得到了两个文本节点对象
</script>
firstChild 与 lastChild —— 第一个/最后一个子节点
[!tip] 这里的节点包括 12 种节点类型,因此它也不太干净
每个节点都有这两个属性,含义如上,如果没有子节点,则为 null
<div>
<span></span>
<span></span>
</div>
<script>
const div = document.querySelector('div')
console.log(div.firstChild, div.lastChild) // 得到了两个文本节点对象
</script>
ownerDocument —— 指向文档节点
[!tip] document 节点是一个抽象根节点对象,没有实际的标签载体,它代表的是整个网页载体,在它下面的第一个具体元素节点为 html 元素
文档内所有节点都有 ownerDocument 属性,指向文档节点,为 document 节点
<div>
<span></span>
<span></span>
</div>
<script>
const div = document.querySelector('div')
console.log(div.ownerDocument) // 得到了 document 节点
</script>
节点关系图
有了以上关系指针,我们可以访问到几乎所有的 DOM 节点
![[z_attachments/Pasted image 20260121232250.png|800]]
操作节点
下面这四个方法都需要先取得他们的父节点,也就说他们都是操作 childNodes 列表的方法,如果在不支持子节点的节点上使用这些方法会抛出错误
appenChild() —— 在 childNodes 列表尾部插入元素
// 语法
parentNode.appenChild(newChild)
该方法返回新添加的节点,会更新相关的关系指针,包括父节点和之前的最后一个子节点,如果把文档中已经存在的节点作为 newNode 插入,则相当于将这个节点移动到插入位置(节点并没有变成2份)
insertBefore() —— 在 childNodes 列表特定位置插入节点
将新节点作为参照节点的同胞节点插入到参照节点前面,与 appendChild() 相同,他也会更新相关关系指针,不会删除任何已有节点
[!tip] 如果参照节点为 null 则 insertBefore() 与 appendChild() 效果一致
// 语法
// 新节点 参照节点
parentNode.insertBefore(newChild,referenceChild)
replaceChild() —— 替换节点
接收插入节点和被替换节点两个参数,被替换节点会从文档树中完全移除,插入节点取而代之,方法返回被替换的节点
// 语法
parentNode.replaceChild(newChild, oldChild)
被替换的节点从技术上说仍然被同一个文档所拥有,但文档中已经没有它的位置
removeChild() —— 在 childNodes 列表中移除节点
方法返回被移除的节点
// 语法
parentNode.removeChild(removeChild)
被移除的节点从技术上说仍然被同一个文档所拥有,但文档中已经没有它的位置
操作节点(不需要 parentNode)
cloneNode() —— 复制节点
返回复制的结果,是一个一模一样的节点,复制的节点在技术上来说属于文档,但尚无父节点,这种节点被称为孤儿节点,可以通过 appendChild() , insertBefore() , replaceChild() 方法添加到文档中
它只复制 HTML 属性,如事件监听等 JS 属性不会复制
// 语法
// 参数默认为 false ,表示只克隆节点引用,若为 true 会深拷贝节点及后代节点
targetNode.cloneNode(clonedeep)
normalize() —— 处理文档子树中的文本节点
在节点上调用 normalize() 方法会检测这个节点的所有后代,如果发现空文本节点,将其删除,如果发现两个同胞文本节点相邻,则合并为一个文本节点
document 类型
[!tip] document 类型是只读的,修改它在非严格模式下会静默失效,严格模式会抛出 TypeError 错误
- 描述 Document 类型是 JavaScript 中表示文档节点的类型,(以下描述的除了指明为 document 类型外,指的都是这个 HTMLDocument 的实例,即 document 对象)在浏览器中,文档对象 document 是HTMLDocument 的实例(HTMLDocument 继承 Document),表示整个 HTML 页面。document 是 window 对象的属性,因此是一个全局对象,它包含以下特征
一、nodeType 等于 9;
二、 nodeName 值为"#document";
三、 nodeValue 值为 null;
四、 parentNode 值为 null;
五、 ownerDocument 值为 null;
六、子节点可以是 DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction 或 Comment 类型
Document 类型可以表示 HTML 页面或其他 XML 文档
访问 document 子节点
document.documentElement 属性 —— 指向页面 html 元素标签
document.body 属性 —— 指向页面 body 元素标签
文档信息
- title 属性 —— 可写 可以读取或修改文档标题(即页面标题,不设置它的情况下,页面标题为 title 标签的值)
document.title
- URL 属性 —— 只读 获取页面的完整 URL 地址(浏览器页面地址栏地址)
document.URL
- domain 属性 —— 有限可写 获取页面的域名
document.domain
- referrer 属性 —— 只读 获取链接到当前页面的那个页面的 URL,例如从 A.com 跳转到了该页面,那么该页面的 referrer 的值为 A.com ,如果当前页面没有来源则它为空字符串
document.referrer
⭐定位元素
document.getElementById
通过 id 名查找元素,返回第一个符合的元素
document.getElementsByTagName
通过标签名查找元素,在 HTML 文档中,这个方法返回一个 HTMLCollection 对象,HTMLCollection 对象和 NodeList 很相似,都具有 length 属性,可以使用 [] 或者 item(n) 获取其中的元素,除此之外 HTMLCollection 对象还具有一些额外的方法
<div>
<img />
<img />
<img name="myImage" />
</div>
<script>
const images = document.getElementsByTagName('img')
// 1. 通过 name 属性获取 HTMLCollection 的指定节点引用
images.namedItem('myImage')
// 2. 通过 [name属性值] 获取 HTMLCollection 的指定节点引用
images['myImage']
</script>
HTMLCollection 对象通过 [] 的数字索引或者字符串索引获取指定引用的操作,在后台本质还是调用 item() 和 namedItem(name) ,NodeList 也是一样的
document.getElementsByTagName(*) —— 获取文档中所有的元素
* 是通配符,用它查找返回的 HTMLCollection 对象包含文档中所有的元素
document.getElementsByName() —— 按 name 属性值查找元素
返回具有给定 name 属性的所有元素,最常用于单选按钮,这里返回的也是 HTMLCollection 对象
// 获取所有 name 属性值为 lcl 的元素,返回 HTMLCollection 对象
const lcl = document.getElementsByName('lcl')
document.getElementsByClassName() —— 按类名查找元素
返回具有给定 name 属性的所有元素,这里返回的也是 HTMLCollection 对象
特殊集合
[!tip] 这些集合返回的也是 HTMLCollection 对象
document.anchors —— 获取文档所有带 name 属性的 a 元素
document.forms —— 获取文档所有 form 元素
与 document.getElementsByTagName ("form") 返回的结果相同
document.images —— 获取文档中所有
元素
与 document.getElementsByTagName ("img") 返回的结果相同
document.links —— 获取文档中所有带 href 属性的 a 元素
document.implementation —— Dom 兼容性检测
需要时自己查如何检测
文档写入
// 像页面输出内容,如果页面意见加载完成了,书写的内容会替代页面内容
document.write()
// 相比上者,该方法会在末尾添加一个换行符
document.writeln()
// 打开关闭网页网页输出流
document.open()
document.close()
⭐ Element 类型
除了Document 类型,Element 类型就是Web开发中最常用的类型了。Element 表示XML或HTML元素,对外暴露出访问元素标签名、子节点和属性的能力。Element 类型的节点具有以下特征:
一、nodeType 等于 1
二、 nodeName 值为元素的标签名
三、 nodeValue 值为 null
四、 parentNode 值为 Document 或 Element 对象
五 子节点可以是 Element、Text、Comment、ProcessingInstruction、CDATASection、EntityReference 类型
nodeName tagName —— 获取 Element 标签名
这两个属性的值是相同的,添加后者明显是为了防止前者被误会
const div = document.getElementsByTagName('div')
console.log(div[0].tageName) // DIV
[!tip] 在 HTML 中,标签名始终为全大写,在 XML 中则与源代码保持一致,因此可以先转小写再比较,适用两个平台
HTML 元素
这里的 HTML 元素指的不是 html 标签,而是包括 div a img 等所有的 html 元素
标准属性
所有 HTML 元素都通过 HTMLElement 类型表示,包括其直接实例和间接实例。另外,HTMLElement,直接继承 Element 并增加了一些属性
这些元素都是可写的
| 属性名 | 含义 | 备注 |
|---|---|---|
| id | 元素在文档中的唯一标识符 | id 属性的值 |
| title | 包含元素的额外信息,通常以提示条形式展示 | |
| lang | 元素内容的语言代码(很少用) | |
| dir | 语言书写方向 | rtl 为 右向左,ltr 为从左向右 |
| className | 相当于 class 属性,用于指定元素的 CSS 类 | 因为 class 是 ECMAScript 关键字,所以不能直接用这个名字,而是取名 className |
操作属性
通过 HTMLElement 获取属性
除了上面 5 个所有 html 都有的标准属性,一些标签还扩展了自己的标准属性,例如 table.rows 可以获取 table 标签的所有行引用,用这个方法只能获取该标签的标准属性,如 my-property 这样的自定义属性就不能获取了,可以通过 dataset 带 data-v 开头的自定义属性,其他自定义属性则不能通过 DOM 对象直接获取了
getAttribute() —— 获取标准属性和自定义属性的值
[!tip] 属性名不区分大小写,因此 id 和 ID 是同一个属性
它的本质是能获得所有属性的字符串值
需要注意的是,用它获取类名属性需要传入 class 作为属性名,只有通过对象访问时才需要写 className
有两种属性,使用 DOM 对象和 getAttribute() 获得的值是不同的
一、style 属性跟通过 el.style 获取的不同
通过 DOM 对象获取的 style 是一个(CSSStyleDeclaration)对象,而 getAttribute() 获取的 style 属性是一个字符串,因为 style 属性用于以编程方式读写元素样式,因此不会直接映射为元素中 style 属性的字符串值
二、事件处理程序(或者事件属性)
DOM 对象获取的事件属性,值是一段 JS 代码(没有该属性则返回 null ),而通过 getAttribut() 则获得这段 JS 源码的字符串
// 点击事件属性
dom.onclick
setAttribute() —— 设置标准属性和自定义属性的值
使用 setAttribute()方法设置的属性名会规范为小写形式,因此 ID 和 id 是等价的
[!tip] 在 dom 对象上直接添加自定义属性,并不会让它成为元素的属性,如下
div.myColor = 'red'
div.getAttribute('myColor') // null IE 除外
使用 setAttribute() 就能将他变成元素的属性
// 设置为元素属性
div.setAttribute('myColor', 'red')
// dom 对象还是不能访问, 为 undefined
console.log(div.myColor)
// 可以访问 'red'
console.log(div.getAttribute('myColor'))
removeAttribute() —— 删除标准属性或自定义属性
它会完全将该属性从元素中删除,平时用的不多,但在序列化 DOM 元素时可以通过它控制要包含的属性
div.removeAttribute("class");
hasAttribute() —— 判断是否含有标准属性或自定义属性
div.removeAttribute("class");
attributes 属性
[!tip] NamedNodeMap 是一个类数组对象
Element 类型是唯一使用 attributes 属性的 DOM 节点类型,attributes 属性性包含一个 NamedNodeMap 实例,是一个类似 NodeList 的“实时”集合,每个属性都表示为一个 Attr 节点,并保存在这个 NamedNodeMap 对象中,attributes 属性有以下方法
attributes 属性中的每个节点的 nodeName 是对应属性的名字,nodeValue 是属性的值。比如,要取得元素 id 属性的值,可以使用以下代码
let id = element.attributes.getNamedItem("id").nodeValue
document.createAttribute(name) —— 以 name 为属性名创建 attr 节点
attr 节点的 nodeValue 或者 value 可以读写属性值
getNamedItem(name) —— 返回 nodeName 等于 name 的节点
这个方法可以用于获取标准属性和自定义属性
[!tip] 注意它返回的是 attr 节点,这个节点打印出来是如 myColor="red" 的形式,但他不是字符串,而是一个对象(太奇怪了长的),通过 nodeName 和 nodeValue 可以获取它的属性名和属性值
// 示例,获取 myColor 属性的值
<body>
<div class="container" myColor="red">
<img />
<img />
<img name="myImage" />
</div>
</body>
<script>
const div = document.getElementsByTagName('div')[0]
console.log(div.attributes.getNamedItem('myColor')) // myColor="red"
// nodeValue 和 value 都能获取 attr 节点的值
console.log(div.attributes.getNamedItem('myColor').nodeValue) // 'red'
console.log(div.attributes.getNamedItem('myColor').value) // 'red'
</script>
使用中括号访问,跟上面具有相同效果
div.attributes['myColor'] // myColor="red"
div.attributes['myColor'].value // 'red'
removeNamedItem(name) —— 删除 nodeName 属性等于 name 的节点
它返回被删除的节点
setNamedItem(node) —— 向列表中添加 node 节点,以其 nodeName 为索引
node 参数为要添加的节点(通常是 Attr 节点,也就是元素的属性节点)
以其 nodeName 为索引是指,会将 attr 节点的 nodeName 作为键,而这个节点本身作为值存储到 attrbutes 属性中(跟前面通过 getNamedItem 获取 attr 节点形成闭环)
如果本来无该节点则新增,有的话就替换旧节点,并且返回旧节点
<body>
<div class="container" myColor="red">
<img />
<img />
<img name="myImage" />
</div>
</body>
<script>
const div = document.getElementsByTagName('div')[0]
const lclAttr = document.createAttribute('lcl')
// 给元素添加了 attr 节点(即给元素添加了属性)
console.log(div.attributes.setNamedItem(lclAttr))
lclAttr.value = '22'
// atrrbutes 属性和 元素都更新了其属性值
console.log(div.attributes.getNamedItem('lcl'), div.getAttribute('lcl'))
</script>
item(pos) —— 返回索引位置 pos 处的节点
与上面按属性名查找的 getNamedItem 方法互补,它按照索引查找,按照元素的属性顺序或者定义顺序查找(从索引 0 开始)(不同浏览器似乎不一定?)
现代浏览器支持等价的 [] 写法
div.attributes.item(0) === aiv.attrbutes[0]
[!tip] 总结,可以通过
attributes[]传入索引或者属性名等价完成 getNamedItem() setNamedItem() 和 item() 的工作
attributes 属性的作用
通过 attributes 属性设置属性/修改属性不如 setAttribute 这一套方法简单,attributes 属性最大的作用在于需要迭代元素上所有属性的时候,如把 DOM 结构序列化为 XML 或 HTML 字符串
// 示例
// 以下代码能够迭代一个元素上的所有属性并以 attribute1="value1" attribute2="value2"的形式生成格式化字符串
function outputAttributes(element) {
let pairs = [];
for (let i = 0, len = element.attributes.length; i < len; ++i) {
const attribute = element.attributes[i];
pairs.push(`${attribute.nodeName}="${attribute.nodeValue}"`);
}
return pairs.join(" ");
}
document.createElement() —— 创建元素
在 HTML 中元素标签不区分大小姐( XML 严格区分)
使用该方法创建新元素的同时会将这个元素的 ownerDocument 属性设置为 document
刚创建的元素还没有加入到文档树中,不会影响浏览器布局,此时应该将他的属性补全(此时不会触发重排重绘),插入到文档树中后再修改会立刻在浏览器中反映出来
元素后代
查找所有 HTML 元素如 document 一样具有 getElementById getElementsByTagName getElemmentsByClassName 方法,只不过他们的查找范围被局限于该元素及其后代的范围内
跳过的内容
例如 TextNode CommentNode AttrNode 动态插入脚本 动态插入样式等节点被我跳过了,如果你有兴趣,可以再前往 JS 高级程序设计进行查阅,跳过的原因是我觉得这辈子应该都用不上这些东西
操作 Table
表格是 HTML 中最复杂的结构之一,通过 DOM 编程创建创建 <table> 元素涉及很多代码,相当繁琐,为此 HTML DOM 给表格元素添加了一些属性和方法
[!tip] pos 指的是索引,从 0 开始
<table> 元素的属性和方法
| 类别 | 名称 | 说明 |
|---|---|---|
| 属性 | caption | 指向 <caption> 元素的指针(如果存在) |
| 属性 | tBodies | 包含 <tbody> 元素的 HTMLCollection |
| 属性 | tFoot | 指向 <tfoot> 元素(如果存在) |
| 属性 | tHead | 指向 <thead> 元素(如果存在) |
| 属性 | rows | 包含表示所有行的 HTMLCollection |
| 方法 | createTHead() | 创建 <thead> 元素,放到表格中,返回引用 |
| 方法 | createTFoot() | 创建 <tfoot> 元素,放到表格中,返回引用 |
| 方法 | createCaption() | 创建 <caption> 元素,放到表格中,返回引用 |
| 方法 | deleteTHead() | 删除 <thead> 元素 |
| 方法 | deleteTFoot() | 删除 <tfoot> 元素 |
| 方法 | deleteCaption() | 删除 <caption> 元素 |
| 方法 | deleteRow(pos) | 删除给定位置的行 |
| 方法 | insertRow(pos) | 在行集合中给定位置插入一行 |
<tbody> 元素的属性和方法
| 类别 | 名称 | 说明 |
|---|---|---|
| 属性 | rows | 包含 <tbody> 元素中所有行的 HTMLCollection |
| 方法 | deleteRow(pos) | 删除给定位置的行 |
| 方法 | insertRow(pos) | 在行集合中给定位置插入一行,返回该行的引用 |
<tr> 元素的属性和方法
| 类别 | 名称 | 说明 |
|---|---|---|
| 属性 | cells | 包含 <tr> 元素所有表元(单元格)的 HTMLCollection |
| 方法 | deleteCell(pos) | 删除给定位置的表元 |
| 方法 | insertCell(pos) | 在表元集合给定位置插入一个表元,返回该表元的引用 |
MutationObserver 接口
- 描述
可以在 DOM 被修改时异步执行回调
可以观察整个文档、DOM 树的一部分,或某个元素。此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化
- 语法 通过调用 MutationObserver 构造函数并传入一个回调函数来创建,新创建的实例并不会跟任何 DOM 进行关联
// 回调函数会在你制定的 DOM 发生变化时触发
// 回调函数有一个 MutationRecord 实例的数组
const callback = (MutationRecord) => {}
const observer = new MutationObserver(callback)
- 回调函数与 MutationRecord MutationRecord 实例包含的信息包括发生了什么变化,以及 DOM 的哪一部分受到了影响
由于回调执行前可能同时发生了多个满足观察条件的变化,因此回调执行时会传入一个按顺序入队的 MutationRecord 实例的数组
MutationRecord 实例
| 属性 | 说明 |
|---|---|
| target | 被修改影响的目标节点 |
| type | 字符串,表示变化的类型:"attributes"、"characterData"或"childList" |
| oldValue | 如果在 MutationObserverInit 对象中启用(attributeOldValue 或 characterDataOldValue 为 true),"attributes"或"characterData"的变化事件会设置这个属性为被替代的值。"childList"类型的变化始终将这个属性设置为 null |
| attributeName | 对于"attributes"类型的变化,这里保存被修改属性的名字。其他变化事件会将这个属性设置为 null |
| attributeNamespace | 对于使用了命名空间的"attributes"类型的变化,这里保存被修改属性的命名空间。其他变化事件会将这个属性设置为 null |
| addedNodes | 对于"childList"类型的变化,返回包含变化中添加节点的 NodeList。默认为空 NodeList |
| removedNodes | 对于"childList"类型的变化,返回包含变化中删除节点的 NodeList。默认为空 NodeList |
| previousSibling | 对于"childList"类型的变化,返回变化节点的前一个同胞 Node。默认为 null |
| nextSibling | 对于"childList"类型的变化,返回变化节点的后一个同胞 Node。默认为 null |
observe()方法 —— 监控 DOM
通过 observe() 方法能将观察者和 DOM 关联起来,该方法需要传入两个参数,要观察其变化的 DOM 节点,以及一个 MutationObserverInit 对象
MutationObserverInit 对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典
// 示例 —— 观察 body 的所有元素变化
const ob = new MutationObserver(() => console.log('body change'))
// 监控所有属性
ob.observe(document.body, { atrribute: true })
document.body.className = 'lcl'
console.log(666)
// 由于 MutationObserver 是异步执行回调,打印结果为
// 666
// body change
// 反应一段属性变化的 observer 回调
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributes: true });
document.body.setAttribute('foo', 'bar');
// [
// {
// addedNodes: NodeList [],
// attributeName: "foo",
// attributeNamespace: null,
// nextSibling: null,
// oldValue: null,
// previousSibling: null
// removedNodes: NodeList [],
// target: body
// type: "attributes"
// }
// ]
// 连续修改会生成多个 MutationRecord 实例,下次回调执行时就会收到包含所有这些实例的数组,顺序为变化事件发生的顺序
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
document.body.className = 'bar';
document.body.className = 'baz';
// [MutationRecord, MutationRecord, MutationRecord]
[!tip] 通过 target 标识可以区分被观察者,因此可以实现一个 observer 观察多个对象
MutationObserverInit 对象 —— 控制对目标节点的观察范围
可以观察属性变化、文本变化和子节点变化
| 属性 | 说明 |
|---|---|
| subtree | 布尔值,表示是否观察目标节点的子树(后代)。 - false:只观察目标节点本身的变化。 - true:观察目标节点及其整个子树。 默认为 false。 |
| attributes | 布尔值,表示是否观察目标节点的属性变化。 默认为 false。 |
| attributeFilter | 字符串数组,表示需要观察的特定属性列表(如 [‘class’, ‘id’])。 设置此参数会自动将 attributes 的值视为 true。 默认为 null(观察所有属性)。 |
| attributeOldValue | 布尔值,表示是否在 MutationRecord 的 oldValue 属性中记录变化前的属性值。 设置此参数会自动将 attributes 的值视为 true。 默认为 false。 |
| characterData | 布尔值,表示是否观察文本节点(字符数据)内容的更改。 默认为 false。 |
| characterDataOldValue | 布尔值,表示是否在 MutationRecord 的 oldValue 属性中记录变化前的文本数据。 设置此参数会自动将 characterData 的值视为 true。 默认为 false。 |
| childList | 布尔值,表示是否观察目标节点子节点的添加或移除操作。 默认为 false。 |
disconnect() —— 停止所有 DOM 监听
默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事件,从而被执行。要提前终止执行回调,可以调用 disconnect()方法
// 语法
observer.disconnect()
[!tip] 使用 disconnect 方法提前断开连接,不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调
// 同步断联,在任务队列中要执行的回调被抛弃
let observer = new MutationObserver(() => console.log('<body> attributes changed'));
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
observer.disconnect();
document.body.className = 'bar';
//(没有日志输出)
// setTimeou 异步回调,保留已经加入任务队列,要执行的回调
let observer = new MutationObserver(() => console.log('<body> attributes changed'));
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
setTimeout(() => {
observer.disconnect();
document.body.className = 'bar';
}, 0);
// <body> attributes changed
disconect 不会结束这个观察者的生命,因此使用它清空观察列表后,可以重新调用 observe() 方法添加被观察者