以下是本人在 js 学习过程中个人总结和积累的一些笔记,我学习或参考过 黑马js、渡一、JavaScript高级程序设计、MDN、al 交流等课程或资料,希望可以帮助到读者,欢迎读者纠偏
本篇是 JS 基础篇 —— 二,包括 Map、Set、事件循环、代理反射等
Map —— 映射
[!tip] 注意
- 与 Object 只能使用整数、字符串和符号作为键不同,Map 可以使用任何 JS 数据类型来作为键
- Map 内部使用 SamValueZero 标准来检查键的匹配性,导致一些问题,如 所有 NaN 被认为是同一个键,+0,0,-0 被认为是不相同的
-
作用 为 JS 带来了真正的键——值存储机制
-
属性
| 属性名 | 值 |
|---|---|
| size | map 键值对的数量 |
- 创建 Map 给 map 传入一个可迭代对象,需包含键值对数组,即可初始化实例,可迭代对象中的键值对会按照迭代顺序插入到新映射实例中
// 1. 创建一个空 Map
const map = new Map()
console.log(map.size) // 0
console.log(map.get(undefined)) // undefined
console.log(map.has(undefined)) // false
// 2. 通过嵌套数组初始化映射
const map1 = new Map([
['key1', 'value1'],
['key2', 'value2'],
['key3', 'value3'],
])
console.log(map1.size) // 3
// 3. 使用自定义迭代器初始化映射
const map2 = new Map({
[Symbol.iterator]: function* () {
yield ['key1', 'value1']
yield ['key2', 'value2']
yield ['key3', 'value3']
}
})
console.log(map2.size) // 3
// 4. 映射期待值为键值对,无论是否提供
const map3 = new Map([])
console.log(map.size) // 0
console.log(map.get(undefined)) // undefined
console.log(map.has(undefined)) // true
基本 API
| 方法名 | 语法 | 含义 | 备注 |
|---|---|---|---|
| set | map.set(key, value) | 添加/修改键值对 | 返回映射实例,可以链式调用 |
| get | map.get(key) | 获取键的值 | 若无该键,则返回 undefined |
| has | map.has(key) | 查询是否有该键 | 返回布尔值 |
| delete | map.delete(key) | 删除某键 | |
| clear | map.clear() | 清空 map 实例 | 删除所有键 |
顺序与迭代
-
描述 Map 与 Object 的一个主要差异为,Map 会维护键值对的插入顺序,可以根据插入顺序进行迭代操作,Map 提供一个迭代器,基于插入顺序生成
[key, value]形式的数组 -
迭代方法
| 方法名 | 语法 | 含义 |
|---|---|---|
| entries | map.entries() | 返回以插入顺序生成[key, value]的迭代器,即 map.entries() === map.[Symbol.iterator] |
| keys | map.keys() | 返回以插入顺序生成键的迭代器 |
| values | map.values | 返回以插入顺序生成值的迭代器 |
// 示例
const map = new Map([
[`key1`, `value1`],
[`key2`, `value2`],
])
for (let k of map.entries()) {
console.log(k)
}
// [`key1`, `value1`]
// [`key2`, `value2`]
[!tips] 注意
- 除了以上方法外,还可以通过 forEach 遍历 Map
将 Map 转换为数组
-
默认迭代器 Map 的默认迭代器是 entries() ,因此可以使用扩展操作符可以将 map 转换为数组
-
示例
// 示例
const map = new Map([
[`key1`, `value1`],
[`key2`, `value2`],
])
consloe.log([...map]) // [[`key1`, `value1`], [`key2`, `value2`]]
WeakMap —— 弱映射
[!tip] 注意
- WeakMap 中的键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置会抛出 TyperError,值的类型没有限制
-
描述 是 Map 的“兄弟”类型,其 API 是 Map 的子集
-
实例化
// 1. 不进行初始化
new WeakMap()
// 2. 进行初始化
const key1 = { id: 1 }
const key2 = { id: 2 }
const key3 = { id: 3 }
const weakMap = new WeakMap([
[key1, value1],
[key2, value2],
[key3, value3]
])
弱键
-
描述 类似于弱引用,如果 WeakMap 中的键没有其他地方引用,则会被垃圾回收机制回收,但是 Map 中的键不会
-
示例
// 执行完第二行代码后,由于没有其他指向该 {} 的引用,该对象键会被当作垃圾回收,如果值也没有被引用,那么键值对被破坏后,值也会称为垃圾回收的目标,wm 成为空映射
const wm = new WeakMap()
wm.set({}, 'value')
方法
- 描述 弱映射具有 set , has , get 和 delete 方法,由于键随时都可能被销毁,因此没有提供 clear 方法
不可迭代键
- 描述 由于其键值对任何时候都可能被销毁, 没有必要提供迭代其键值对的能力,因此不可能在不知道对象引用的情况下从弱映射中取得值,即使代码可以访问 WeakMap 实例,也没办法看到其中的内容
Set —— 集合
-
介绍 set 是一种新的引用类型,为 JS 带来了集合数据结构,它类似于数组,但它的值可以是任何 JS 数据类型,其内部成员的值都是唯一的,没有重复的
-
创建 Set Set 没有字面量,只能通过 new Set() 构造函数创建
// set 不仅可以接收数组,还可以接收其他任何可迭代对象
const arr = [value1, value2,value3]
const set = new Set(arr)
- 特点
- 唯一性,依照"SameValueZero" 算法判断值是否相等,近似于严格相等(===),同时 NaN 也会去重,0 与 -0 、+0 之间互不相等,面对引用数据类型时,引用相同的去重
- 保证插入顺序
实例属性
set.size
- 含义 set 对象的长度
// 示例
const set = new Set([1, 2, 3])
console.log(set.size) // 3
实例方法
set.add(value)
- 作用 添加某个值到 set 中,返回 Set 对象本身,支持链式调用
const set = new Set([1])
set.add(2).add(3)
console.log(set.size) // 3
set.delete(value)
- 作用 删除某个值,返回一个布尔值
const set = new Set([1, 2])
set.delete(2)
console.log(set.has(2)) // flase
set.has(value)
- 作用 查询 set 中是否有该值,并返回一个布尔值
const set = new Set([1, 2])
set.delete(2)
console.log(set.has(1)) // true
set.clear()
- 作用 清空 set ,使它变成一个空集合
const set = new Set([1, 2])
set.claer()
console.log(set.size) // 0
顺序与迭代
-
描述 Set 提供一个迭代器,基于插入顺序生成集合内容
-
迭代方法
| 方法名 | 语法 | 含义 |
|---|---|---|
| entries | set.entries() | 返回以插入顺序生成[value, value]的迭代器 |
| keys | set.keys() | 是 values() 的别名方法,即 set.kes() === set.[Symbol.iterator] |
| values | set.values | 返回以插入顺序生成集合内容的迭代器,即 set.values() === set.[Symbol.iterator] |
遍历 Set
- set.keys() , set.values() , set.entries()
const set = new Set(['a', 'b', 'c']);
// keys() 和 values() 相同
console.log([...set.keys()]); // ['a', 'b', 'c']
console.log([...set.values()]); // ['a', 'b', 'c']
// entries() 返回 [value, value] 形式的数组
console.log([...set.entries()]); // [['a', 'a'], ['b', 'b'], ['c', 'c']]
- set.forEach()
const set = new Set([1, 2, 3]);
set.forEach((value, key, set) => {
console.log(`Value: ${value}, Key: ${key}`);
});
// Value: 1, Key: 1
// Value: 2, Key: 2
// Value: 3, Key: 3
将 Set 转换为数组
-
默认迭代器 由于 values() 是默认迭代器,因此可以通过扩展操作符将 set 示例转换为数组
-
示例
const set = new Set([0, 1, 2])
console.log([...set]) // [0, 1, 2]
WeakSet —— 弱集合
[!tip] 注意
- WeakSet 中的值只能是 Object 或者继承自 Object 的类型,而 Set 的值可以为任何 JS 类型
-
描述 是 Set 的“兄弟”类型,其 API 是 Set 的子集
-
实例化
// 1. 空的实例
new WeakSet()
// 2. 初始化实例
const value1 = { id: 1 }
const value2 = { id: 2 }
const value3 = { id: 3 }
const ws = new WeakSet([value1, value2, value3])
方法
- 描述 没有 clear 方法,其他跟 Set 一致
弱值
- 描述 表示这些值不属于正式的引用,不会阻止垃圾回收
不可迭代值
- 描述 由于 WeakSet 中的值任何时候都有可能被销毁,所以没必要提供迭代其值的能力,基于同样的理由,也没有提供 clear 这样销毁所有值得方法 由于不可迭代,所以不可能在不知道对象引用得情况下从弱集合中取得值,即使代码可以访问 WeakSet 实例,也无法看到其中得内容
弱引用
- 弱引用 在 JS 中,保持对某个对象的引用,但又不阻止它被当作垃圾回收,称为弱引用(即不会阻止对象被作为垃圾回收的引用) 也可以解释为,如果指向某个对象的唯一引用是弱引用,垃圾回收器会释放该对象所占用的内存
WeakRef —— 创建对象弱引用
-
描述 通过 WeakRef 类可以创建对象的弱引用,创建时可以传入一个参数,即弱引用的目标对象
-
示例
const obj = { name: 'lcl' }
const weakRef = new WeakRef(obj)
- 方法
| 方法名 | 语法 | 含义 | 返回值 |
|---|---|---|---|
| deref | weakRef.deref() | 访问被弱引用的目标对象 | 目标对象(如已被回收则返回 undefined ) |
❌ FinalizationRegistry —— 垃圾清理钩子 ❌
[!tips] 注意
- 由于垃圾回收机制行为的不确定性,应该尽可能不使用该类
-
描述 FinalizationRegistry 对象可以定义一个回调函数,这个回调函数在注册的对象被回收之前执行
-
创建 FinalizationRegistry 实例
const finalizationRegistry = new FinalizationRegistry((heldValue) => {
})
register() —— 注册对象,传递持有值
-
作用 注册对象,传递持有值
-
语法
// heldValue 持有值,该值在垃圾回收之前被传给处理函数
finalizationRegistry.register(obj, heldValue)
unregister() —— 注销对象
-
参数 一个参数,传入被注册的对象
-
语法
// obj 为被注册过的对象
finalizationRegistry.unregister(obj)
Clipboard API —— 剪贴板命令
-
含义 剪贴板 Clipboard API 提供了响应剪贴板命令(剪切、复制和粘贴)与异步读写系统剪贴板的能力
-
访问
// 通过 navigator.clipboard 访问
navigator.clipboard
ClipboardItem
-
含义 是异步 Clipboard API 的核心接口,它定义了剪贴板中数据项的标准化表示形式
-
创建一个 ClipboardItem 创建 ClipboardItem 时,需要传入一个对象,其键是 MIME 类型,其值是对应数据的 Blob 对象或者其值会被解析为 Blob 对象的 Promise
// 示例,创建一个包含纯文本和 html 格式的 ClipboardItem
// 准备 HTML 内容
const htmlContent = "<p>这是一段<strong>加粗</strong>的文字。</p>";
const htmlBlob = new Blob([htmlContent], { type: "text/html" });
// 准备纯文本内容
const plainTextContent = "这是一段加粗的文字。";
const textBlob = new Blob([plainTextContent], { type: "text/plain" });
// 创建 ClipboardItem
const clipboardItem = new ClipboardItem({
"text/html": htmlBlob,
"text/plain": textBlob,
});
实例方法和属性
| 属性/方法 | 说明 |
|---|---|
types (只读) | 返回一个数组,包含该 ClipboardItem 所有可用的 MIME 类型,例如 ["text/html", "text/plain"]。 |
getType(type) | 根据指定的 MIME 类型,返回一个 Promise,该 Promise 解析为对应数据的 Blob 对象。 |
静态方法 —— supports()
-
作用 根据 MIME 类型,检验浏览器剪贴板是否支持该类型,若支持则返回 true ,反之返回 false
-
语法
// type 的值为 MIME 类型
ClipboardItem.supports(type)
writeText()
-
作用 将指定文本写入系统剪贴板,并返回一个在剪贴板内容更新后兑现的 Promise
-
语法
// newClipText 为要写入剪贴板的文本信息
navigator.clipboard.writeText(newClipText)
readText()
- 作用 返回一个兑现为系统剪贴板中的文本内容的 Promise
navigator.clipboard.readText()
read()
- 作用 用于请求获取剪贴板的内容,返回一个兑现为包含剪贴板内容的 ClipboardItem 对象数组的 Promise
[!tips] 注意
- ClipboardItem 对象数组 的含义是,[ClipboardItem, ClipboardItem, ClipboardItem...] ,即数组中的元素为 ClipboardItem 对象
write()
- 作用 将任意 ClipboardItem 数据(如图像和文本)写入剪贴板,返回一个在操作完成时兑现的 Promise
[!tips] 注意
- 如果底层操作系统不支持系统剪贴板上的多个原生剪贴板项,则只会写入数组中的第一个
- 语法
// data 为要写入剪贴板的 ClipboardItem 对象数组,即 [ClipboardItem.........]
write(data)
遍历对象、可迭代对象
for...in
-
作用 遍历对象属性名或索引
-
具体情况
| 对象类型 | 是否可用 | 遍历行为 | 示例代码 | 输出结果 |
|---|---|---|---|---|
| 普通对象 | ✅ 可用 | 遍历对象自身的可枚举属性名 | const obj = {a:1, b:2}; for(let key in obj) { console.log(key); } | "a", "b" |
| 数组 | ✅ 可用 | 遍历数组索引和自定义属性名 | const arr = [10,20]; arr.custom = 'prop'; for(let key in arr) { console.log(key); } | "0", "1", "custom" |
| 字符串 | ✅ 可用 | 遍历字符串字符的索引 | const str = "hi"; for(let key in str) { console.log(key); } | "0", "1" |
| Map | ✅ 可用 | 遍历Map对象的属性名(通常为空) | const map = new Map([['a',1]]); for(let key in map) { console.log(key); } | (通常无输出) |
| Set | ✅ 可用 | 遍历Set对象的属性名(通常为空) | const set = new Set([1,2]); for(let key in set) { console.log(key); } | (通常无输出) |
| 函数 | ✅ 可用 | 遍历函数的自定义属性 | function fn(){}; fn.prop = 'value'; for(let key in fn) { console.log(key); } | "prop" |
| Date | ✅ 可用 | 遍历Date对象的属性名 | const date = new Date(); date.custom = 'prop'; for(let key in date) { console.log(key); } | "custom" |
| Arguments | ✅ 可用 | 遍历arguments对象的索引 | function test(){ for(let key in arguments) { console.log(key); } } test(1,2) | "0", "1" |
for...of
-
作用 遍历可迭代对象,遍历的是值
-
具体行为 调用可迭代对象的迭代器(即使用 next 方法迭代)将方法返回对象的 value 属性赋值给循环变量,并执行循环体,当 done 为 true 时,忽略本次迭代的 value 值,不执行循环体,并结束迭代,因此,for...of 迭代一个长度为3的数组,会迭代4次,但最后一次结果被忽略,其他情景类似
// 可以通过这个代码了解其具体行为
console.log('--- 验证 for...of 的 next() 调用次数 ---');
const arr = ['A', 'B', 'C'];
// 1. 劫持数组的迭代器
const originalIterator = arr[Symbol.iterator]();
// 2. 创建一个代理对象
const spyIterator = {
next() {
console.log('>>> 🕵️♀️ next() 被调用了!');
const result = originalIterator.next();
console.log(` 返回结果: { value: ${result.value}, done: ${result.done} }`);
return result;
},
[Symbol.iterator]() { return this; }
};
console.log('开始 for...of 循环:');
let loopCount = 0;
for (const item of spyIterator) {
loopCount++;
console.log(` 🔄 循环体执行第 ${loopCount} 次, item = ${item}`);
}
console.log('循环结束。');
// 结果
// --- 验证 for...of 的 next() 调用次数 ---
// 开始 for...of 循环:
// >>> 🕵️♀️ next() 被调用了!
// 返回结果: { value: A, done: false }
// 🔄 循环体执行第 1 次, item = A
// >>> 🕵️♀️ next() 被调用了!
// 返回结果: { value: B, done: false }
// 🔄 循环体执行第 2 次, item = B
// >>> 🕵️♀️ next() 被调用了!
// 返回结果: { value: C, done: false }
// 🔄 循环体执行第 3 次, item = C
// >>> 🕵️♀️ next() 被调用了!
// 返回结果: { value: undefined, done: true }
// 循环结束。
- 具体情况
| 对象类型 | 是否可用 | 遍历行为 | 示例代码 | 输出结果 |
|---|---|---|---|---|
| 普通对象 | ❌ 不可用 | 普通对象不是可迭代对象 | const obj = {a:1, b:2}; for(let value of obj) { console.log(value); } | TypeError |
| 数组 | ✅ 可用 | 遍历数组元素的值 | const arr = [10,20]; for(let value of arr) { console.log(value); } | 10, 20 |
| 字符串 | ✅ 可用 | 遍历字符串的字符 | const str = "hi"; for(let char of str) { console.log(char); } | "h", "i" |
| Map | ✅ 可用 | 遍历Map的键值对数组 | const map = new Map([['a',1],['b',2]]); for(let [k,v] of map) { console.log(k,v); } | "a" 1, "b" 2 |
| Set | ✅ 可用 | 遍历Set的元素值 | const set = new Set([1,2,3]); for(let value of set) { console.log(value); } | 1, 2, 3 |
| TypedArray | ✅ 可用 | 遍历类型化数组的元素 | const arr = new Uint8Array([1,2,3]); for(let value of arr) { console.log(value); } | 1, 2, 3 |
| Arguments | ✅ 可用 | 遍历arguments对象的值 | function test(){ for(let value of arguments) { console.log(value); } } test(1,2) | 1, 2 |
| NodeList | ✅ 可用 | 遍历DOM节点列表的节点 | for(let node of document.querySelectorAll('div')) { console.log(node); } | 各个div元素 |
| 生成器 | ✅ 可用 | 遍历生成器产生的值 | function* gen() { yield 1; yield 2; } for(let value of gen()) { console.log(value); } | 1, 2 |
区别
| 特性 | for...in | for...of |
|---|---|---|
| 用途 | 遍历对象的 可枚举属性 | 遍历 可迭代对象(如数组、字符串等) |
| 返回值 | 返回 键(属性名) | 返回 值(元素值) |
| 适用范围 | 对象、数组(不推荐用于数组) | 数组、字符串、Set、Map等可迭代对象 |
| 是否遍历原型链 | 会遍历原型链上的可枚举属性 | 不会遍历原型链 |
MIME 类型
-
含义 全称多用途互联网邮件扩展(Multipurpose Internet Mail Extensions),是一种用于描述除 ASCII 文本以外的其他格式文档的标准,用于指示文件类型的字符串,会与文件同时发送,描述了内容的格式
-
常见的 MIME 类型列表 常见 MIME 类型列表 - HTTP | MDN
charset
-
含义 字符集,定义文本字符与二进制编码映射关系的技术标准,如 UTF-8 , GBK , GB18030
-
作用 将二进制数据解读为其他数据的 “密码本”
-
最常用 UTF-8 —— 统一覆盖全球所有书写系统的字符,支持多种语言,解决了多语言文本共存的问题
应用
- HTML5 字符编码声明
// 强制标准,指定 html 文件使用 UTF-8 字符集进行编码
<meta charset="UTF-8">
- 作为 MIME 类型的参数
'application/json; charset=utf-8'
// 完整示例,配置请求头
// 发送JSON请求,明确指定MIME类型和字符集
fetch('/api', {
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify({ name: "张三" })
});
Blob(Binary larget object)
-
含义 表示二进制大对象 ,是 JS 对不可修改二进制数据的封装类型
-
创建方式 含字符串的数组,ArrayBuffers ,ArrayBufferViews , Blob 对象都可以用来创建 Blob,下面仅展示含字符串的数组和 Blob 对象创建 Blob 对象的方式
[!tips] 注意
- 创建 Blob 对象包含两个参数,第一个参数是用来创建 Blob 的数据,第二个是 options
- 参数一是一个数组,每个元素都是用来创建 Blob 的数据,每个元素的类型可以不同,参数1一定要是可迭代对象
- 创建时可以传入一个 options 对象,并在其中指定 MIME 类型
// 使用含字符串数组创建
const stringBlob = new Blob(['Hello', ' ', 'World'], { type: 'text/plain' })
// 使用 Blob 对象创建
创建时可以传入一个 options 参数,并在其中指定 MIME 类型
const originalBlob = new Blob(['原始数据']);
const newBlob = new Blob([originalBlob], { type: 'application/octet-stream' });
实例属性
| 属性/方法 | 说明 |
|---|---|
| size | 属性,表示 Blob 对象存储的字节数 |
| type | 属性,表示 Blob 对象的 MIME 类型,未知类型则为空字符串,包括 MIME 类型的参数,如"text/plain;charset=utf-8" |
[!tips] 注意
- 这些属性都是只读的
实例方法 —— slice()
-
作用 创建并返回一个新的
Blob对象,该对象包含调用它的 blob 的子集中的数据,可以切分数据 -
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
| start | 被拷贝进新 Blob 的第一个 索引 ,若为负则从数据末尾向前计算,-10 代表倒数第 10 个字节 | 0 |
| end | 第一个不会被拷贝进新的 Blob 的字节的索引,传入负数如上 | blob.size |
| contentType | 设置新 Blob 的内容类型 | 空字符串 |
[!tips] 注意
- 若 start < end ,start > blob.size,则返回一个空 Blob 对象
- 若 end > blob.size ,则 end 取默认值 blob.size
- 语法
slice(start, end, contentType)
- 示例
const blob = new Blob(["Hello, Blob!"], {
type: "text/plain;charset=utf-8"
});
// 属性
console.log(blob.size); // 11 (字节数)
console.log(blob.type); // "text/plain;charset=utf-8"
// 方法
const slicedBlob = blob.slice(0, 5, "text/plain");
console.log(slicedBlob.size); // 5
// 转换为其他格式
blob.text().then(text => {
console.log(text); // "Hello, Blob!"
});
blob.arrayBuffer().then(buffer => {
console.log(buffer); // ArrayBuffer
});
实例方法 —— text()
-
作用 返回一个 Promise,其会兑现一个包含 blob 内容的 UTF-8 格式的字符串,它没有参数
-
示例
// 以上述 slice 方法的例子为例
blob.text().then(text => {
console.log(text); // "Hello, Blob!"
});
File
[!tip] 注意
- File 和 Blob 对象都只保存了文件的基本信息,如 大小,修改时间,名称,类型,文件位置等,并没有保存文件的数据,因此切分 File 和 Blob 是很快的,本质就是数学运算,真正需要读取数据时,需要使用 FileReader 读取
- 含义
提供有关文件的信息,并允许网页中的 JavaScript 访问其内容,构造函数
new File()返回一个新构建的File对象File对象是一种特殊类型的Blob对象,它包含了Blob的所有功能,并额外添加了一些与文件系统相关的属性
创建方式
-
通过构造函数创建
-
通过 input 元素获取
// 1. 通过构造函数创建
const file = new File(
["文件内容"],
"example.txt",
{
type: "text/plain",
lastModified: new Date().getTime()
}
);
// 2. 从 input 元素获取
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", function(event) {
const files = event.target.files;
if (files.length > 0) {
const file = files[0];
console.log("文件名:", file.name);
console.log("文件大小:", file.size);
console.log("文件类型:", file.type);
}
});
实例属性
| 属性/方法 | 说明 |
|---|---|
| size | 属性,表示 File 对象存储的字节数 |
| type | 属性,表示 File 对象的 MIME 类型,未知类型则为空字符串包括 MIME 类型的参数,如"text/plain;charset=utf-8" |
| name | 文件的名称 |
| lastModified | 文件最后修改的时间戳 |
实例方法
- [[#实例方法 —— slice()|File 对象继承了所有 Blob 对象的实例方法]]
File 与 Blob 的区别
| 特性 | Blob | File |
|---|---|---|
| 文件名 | 无 | 有 (name 属性) |
| 最后修改时间 | 无 | 有 (lastModified) |
| 创建方式 | new Blob() | new File() 或文件选择 |
| 继承关系 | 基类 | 继承自 Blob |
| 使用场景 | 二进制数据存储 | 文件操作 |
FileReader
-
作用 允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容
-
FileReader 构造函数 返回一个 FileReader 对象
[!tips] 注意
- FileReader 中所有的操作都是异步的
实例属性
| 属性 | 说明 |
|---|---|
| error | 在读取文件时发生的错误的 DOMException |
| readyState | 取值为 0(EMPTY)没有加载数据 1(LOADING)数据正在被加载 2(DONE) 数据加载完成 |
| result | 文件的内容,仅在读取操作完成后才有效 |
实例方法 —— abort()
-
作用 中止读取操作,无参数和返回值,返回后 readyState 为 DOEN
-
语法
fileReader.abort()
实例方法 —— readAsArrayBuffer()
-
作用 开始读取指定 Blob 或 File 的内容,无返回值,读取操作完成后 result 属性的值为一个表示文件数据的 ArrayBuffer
-
语法
// blob 为被读取的 Blob 对象或 File 对象
fileReader.readAsArrayBuffer(blob)
实例方法 —— readAsDataURL()
-
作用 开始读取指定 Blob 或 File 的内容,无返回值,读取操作完成后 result 属性包含作为
data:URL的数据,将文件的数据表示为 base64 编码字符串 -
语法
// blob 为被读取的 Blob 对象或 File 对象
fileReader.readAsDataURL(blob)
实例方法 —— readAsText()
-
作用 开始读取指定 Blob 或 File 的内容,无返回值,读取操作完成后 result 属性包含表示文件内容的文本字符串
-
语法
// blob 为被读取的 Blob 对象或 File 对象
fileReader.readAsText(blob)
事件
| 事件 | 说明 |
|---|---|
| abort | 调用 abort() 方法后触发,该事件不会冒泡,不可取消 |
| error | 发生错误时触发,该事件不会冒泡,不可取消 |
| load | 在成功读取文件时触发,该事件不会冒泡,不可取消 |
| loadend | 在文件读取完成(无论成功与否)时触发,若读取成功则在 load 事件后,该事件不会冒泡,不可取消 |
| loadstart | 在文件读取操作开始时触发,该事件不会冒泡,不可取消 |
| progress | 在 FileReader 读取数据中每 50 ms 触发一次,该事件不会冒泡,不可取消 |
核心事件参数
| 事件类型 | 核心属性 | 属性说明 |
|---|---|---|
| loadstart | lengthComputable | 布尔值,表示进度是否可计算(通常为 false) |
loaded | 已读取的字节数(开始时通常为 0) | |
total | 总共要读取的字节数 | |
| progress | lengthComputable | 布尔值,表示进度是否可计算(通常为 true) |
loaded | 当前已读取的字节数 | |
total | 文件总字节数 | |
| load | lengthComputable | 布尔值,表示进度是否可计算(通常为 true) |
loaded | 已读取的字节数(等于 total) | |
total | 文件总字节数 | |
| loadend | lengthComputable | 布尔值,表示进度是否可计算 |
loaded | 最终已读取的字节数 | |
total | 文件总字节数 | |
| error | lengthComputable | 布尔值,表示进度是否可计算 |
loaded | 错误发生时已读取的字节数 | |
total | 文件总字节数 | |
| abort | lengthComputable | 布尔值,表示进度是否可计算 |
loaded | 中止时已读取的字节数 | |
total | 文件总字节数 |
FormData
-
描述 提供了一种表示表单数据的键值对
key/value的构造方式,如果送出时的编码类型被设为"multipart/form-data",它会使用和表单一样的格式 -
创建 FormData 实例
// 方式一
const formData = new FormData()
// 方式二 —— 基于现有表单创建
<form id="userForm">
<input type="text" name="name" value="张三">
<input type="email" name="email" value="zhang@example.com">
<input type="file" name="avatar">
</form>
<script>
// 直接从表单元素创建 FormData
const form = document.getElementById('userForm')
const formData = new FormData(form)
// 会自动包含表单中的所有字段
// formData 已经包含: name, email, avatar
</script>
方法
[!tip] 注意
- append 和 set 的区别在于,遇见重复 key 时,append 添加的值会跟该字段原有的值组成一个数组,作为该字段的值,而 set 会用后值覆盖新值
// 添加字段
formData.append(key, value)
// 检查是否存在某个字段,返回布尔值
formData.has(key) // true
// 获取字段值,具有多个同名字段时返回第一个值
formData.get(key) // '张三'
// 获取所有同名字段的值,并返回一个数组
formData.getAll(key) // ['篮球', '游泳']
// 删除字段
formData.delete(key)
// 设置/替换字段值
formData.set(key,value)
URL
- 描述 URL(Uniform Resource Locator,统一资源定位符)是互联网的核心概念之一,用于定位和访问网络资源。让我为你全面介绍 URL 的各个方面。
URL 的基本结构
一个完整的 URL 包含多个组成部分:
https://www.example.com:8080/path/to/page?name=value#section
\___/ \_____________/ \__/ \_________/ \_______/ \_____/
| | | | | |
协议 主机名 端口 路径 查询参数 片段标识
URL 的各个组成部分
协议(Protocol)
// 常见协议
http:// // 超文本传输协议
https:// // 安全的 HTTP
ftp:// // 文件传输协议
file:// // 本地文件
mailto: // 电子邮件
data: // 数据 URL
主机名(Hostname)
// 主机名类型
www.example.com // 域名
192.168.1.1 // IP 地址
localhost // 本地主机
端口(Port)
// 常见默认端口
http://example.com // 默认端口 80
https://example.com // 默认端口 443
ftp://example.com // 默认端口 21
// 显式指定端口
http://localhost:3000 // 开发服务器
https://api.com:8443 // 自定义 HTTPS 端口
路径(Path)
// 路径示例
/about
/users/123/profile
/api/v1/products
查询参数(Query String)
// 查询参数格式
?key=value
?name=john&age=25
?category=books&sort=price&page=1
片段标识(Fragment)
// 片段标识(锚点)
#section1
#top
#comments
特殊的 URL 类型
Data URLs
// 语法:data:[<mediatype>][;base64],<data>
// 文本数据
const textData = 'data:text/plain;charset=UTF-8,Hello%20World'
const htmlData = 'data:text/html,<h1>Hello</h1>'
// Base64 编码的图片
const imageData = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA...'
// 使用示例
const img = document.createElement('img')
img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>'
Blob URLs
// 创建 Blob URL
const blob = new Blob(['Hello, World!'], { type: 'text/plain' })
const blobUrl = URL.createObjectURL(blob)
console.log(blobUrl) // "blob:https://example.com/550e8400-e29b-41d4..."
// 使用 Blob URL
const link = document.createElement('a')
link.href = blobUrl
link.download = 'hello.txt'
link.textContent = '下载文件'
// 释放 URL(重要!)
URL.revokeObjectURL(blobUrl)
File URLs
// 文件 URL 格式
const fileUrl = 'file:///C:/Users/Name/Documents/file.txt'
const fileUrlUnix = 'file:///home/user/document.pdf'
// 注意:现代浏览器出于安全考虑限制 file:// 协议的使用
URL 编码和解码
编码函数对比
const original = 'hello world & good=bad'
// encodeURIComponent / decodeURIComponent
const encodedComponent = encodeURIComponent(original)
// "hello%20world%20%26%20good%3Dbad"
const decodedComponent = decodeURIComponent(encodedComponent)
// "hello world & good=bad"
// encodeURI / decodeURI
const encodedURI = encodeURI(original)
// "hello%20world%20&%20good=bad"
const decodedURI = decodeURI(encodedURI)
// "hello world & good=bad"
// 区别:
// encodeURIComponent 编码更多字符,适合查询参数
// encodeURI 编码较少字符,适合整个 URL
实际应用
// 构建带参数的 URL
function buildURL(base, params) {
const url = new URL(base)
Object.keys(params).forEach(key => {
url.searchParams.set(key, params[key])
})
return url.toString()
}
const url = buildURL('https://api.example.com/search', {
q: 'javascript tutorial',
page: '1',
sort: 'relevance'
})
// "https://api.example.com/search?q=javascript+tutorial&page=1&sort=relevance"
浏览器中的 URL API
获取当前页面 URL
// 当前页面 URL 信息
console.log(window.location.href) // 完整 URL
console.log(window.location.origin) // 协议 + 主机 + 端口
console.log(window.location.protocol) // 协议
console.log(window.location.host) // 主机 + 端口
console.log(window.location.hostname) // 主机名
console.log(window.location.port) // 端口
console.log(window.location.pathname) // 路径
console.log(window.location.search) // 查询参数
console.log(window.location.hash) // 片段标识
// 修改 URL(不会刷新页面)
window.history.pushState({}, '', '/new-path')
window.location.hash = 'new-section'
相对 URL 解析
// 基于基础 URL 解析相对路径
const baseURL = 'https://example.com/api/v1/'
const relativePath = '../v2/users'
const absoluteURL = new URL(relativePath, baseURL)
console.log(absoluteURL.toString())
// "https://example.com/api/v2/users"
URL 构造函数
创建 URL 实例
- 描述 可以通过绝对路径或者相对路径创建 URL 实例
// 绝对地址
new URL(url)
// 相对地址
new URL(url, base)
- 示例
// 绝对路径
new URL('https://example.com/path/to/page?query=value#section')
// 相对路径
const url = new URL('https://example.com/');
// 修改各个部分
url.protocol = 'http:';
url.hostname = 'subdomain.example.com';
url.pathname = '/new/path';
url.search = '?new=param';
url.hash = '#new-section';
console.log(url.href); // http://subdomain.example.com/new/path?new=param#new-section
URL 静态成员
[!tip] 注意
- ⭐
createObjectURL(blob/file)⭐可以创建指向Blob 或 File 对象的临时 URL,可以作为 src 传入 img 显示,- 虽然使用上述方法创建的 URL 会随着页面销毁而销毁,但还是建议使用
revokeObjectURL(url)在不需要时将临时 url 移除,释放内存
| 静态方法 | 作用 | 用法示例 | 示例结果 |
|---|---|---|---|
createObjectURL(blob/file) | 创建指向 Blob/File 的临时 URL | URL.createObjectURL(file) | "blob:https://<br><br>example.com/550e8400" |
revokeObjectURL(url) | 释放临时 URL 的内存 | URL.revokeObjectURL(blobUrl) | undefined (无返回值) |
canParse(url) | 检查 URL 是否可解析 | URL.canParse('invalid') | false |
URL 实例属性
| 属性 | 作用 | 示例结果 |
|---|---|---|
href | 完整 URL 字符串 | "https://www.example.com:8080/path/page?name=john&age=25#section" |
protocol | 协议(含冒号) | "https:" |
hostname | 主机名 | "www.example.com" |
port | 端口号 | "8080" |
host | 主机名 + 端口 | "www.example.com:8080" |
pathname | 路径部分 | "/path/page" |
search | 查询字符串(含问号) | "?name=john&age=25" |
searchParams | URLSearchParams 对象 | URLSearchParams {"name" => "john", "age" => "25"} |
hash | 片段标识(含井号) | "#section" |
origin | 协议 + 主机 + 端口 | "https://www.example.com:8080" |
username | 用户名 | "" (空字符串) |
password | 密码 | "" (空字符串) |
URL 实例方法
| 方法 | 作用 | 用法示例 | 示例结果 |
|---|---|---|---|
toString() | 返回完整 URL | exampleUrl.toString() | 同 href 属性 |
toJSON() | 返回完整 URL | exampleUrl.toJSON() | 同 href 属性 |
使用示例
const params = exampleUrl.searchParams
// append vs set
params.append('tag', 'js') // 添加 tag=js
params.append('tag', 'css') // 添加 tag=css
params.set('category', 'web') // 设置 category=web
params.set('category', 'tech') // 覆盖为 category=tech
console.log(params.get('tag')) // "js" (第一个值)
console.log(params.getAll('tag')) // ["js", "css"] (所有值)
console.log(params.get('category')) // "tech" (覆盖后的值)
[!tips] 注意
origin是只读属性,无法直接修改searchParams是实时对象,修改会直接影响 URLtoString()和toJSON()返回相同结果- 静态方法主要用于处理 Blob/File 对象
import.meta.url
- 作用 是 ES 模块中的一个特殊属性,它提供了当前模块文件的完整 URL
URLSearchParams 构造函数
-
作用 将查询参数转换为对象,或将对象转换为查询参数
-
示例
// 1. 使用构造函数将参数对象转化为参数字符串
const qObj = {
a: 'lcl',
b: 'dhh'
}
// 将参数对象转换为url参数构造函数
const paramsObj = new URLSearchParams(qObj)
// 使用构造函数方法转换为参数字符串
const queryString = paramsObj.toString()
console.log(queryString) //a=lcl&b=dhh
// 拼接url参数
const url = `http://ajax-base-api-t.itheima.net/api/getbooks?${queryString}`
console.log(url) //http://ajax-base-api-t.itheima.net/api/getbooks?a=lcl&b=dhh
// 2. 将查询字符串转换为参数对象
const queryString = 'a=lcl&b=dhh'
const params = new URLSearchParams(queryString)
const paramsObject = Object.fromEntries(params.entries())
console.log(paramsObject) // {a: 'lcl', b: 'dhh'}
| 方法 | 作用 | 区别说明 |
|---|---|---|
append(key, value) | 添加参数 | 可添加多个同名参数 |
set(key, value) | 设置参数 | 覆盖同名参数,只保留一个 |
get(key) | 获取参数值 | 返回第一个匹配的值 |
getAll(key) | 获取所有值 | 返回同名参数的所有值数组 |
has(key) | 检查参数存在 | 返回布尔值 |
delete(key) | 删除参数 | 删除所有同名参数 |
entries() | 获取键值对迭代器 | 可转换为数组 |
调试工具进阶
命令面板
-
打开方式 在已经打开了控制面板的情况下, ctrl + shift + p,可以调出命令面板
-
截图 输入截图,即可获得以下选项,全尺寸屏幕截图即将该网站全部内容截图 ,选择某一个 dom 节点后,点击 捕获节点屏幕截图 即可单独下载该节点的图片
![[z_attachments/Pasted image 20251022134310.png|600]]
- 停靠位置 输入停靠,即可选择控制面板的停靠位置
元素面板中的查找
-
文本查找
-
CSS 选择器查找
-
Xpath 查找
// // 代表全局,之后像路径一样一层层查找,只适用于标签
//标签1/标签2
- 在控制台查找
// 通过该方式选中后会条转到定位节点的位置
inspect(document.querySelector())
元素面板中查看部分伪类选择器激活后的效果
- 点击 :hov 按钮
![[z_attachments/Pasted image 20251022140213.png]]
元素面板中操控被选中元素的 class
- 点击 .cls 按钮
![[z_attachments/Pasted image 20251022140335.png]]
复制样式并粘贴
- 选中需要复制的 dom 右键,选择复制样式,此时剪贴板会复制这个 dom 的样式
![[z_attachments/Pasted image 20251022140540.png|500]]
- 此时可以选择粘贴到目标 dom 中
![[z_attachments/Pasted image 20251022140827.png|500]]
控制台 (console)
-
快捷键 ctrl + shift + j
-
执行语句 > 代表是执行语句, < 代表上方执行语句的返回值 0 可以打印该元素, 2 上上次,以此类推 console.clear() 清空控制台 console.group(name) 和 console.endGroup(name) 会将中间的 console.log() 作为一个组包起来打印 console.time() 和 console.timeEnd() 会打印他们中间的代码执行的时间 console.table(arr) 可以将数组打印成一个表格,可以进行排序
监控某个变量
- 点击该按钮并且在表达式一栏输入要监控的变量
![[z_attachments/Pasted image 20251022142338.png|600]]
惰性函数
-
核心 第一次调用时修改自身,后续调用直接执行修改后的逻辑,避免重复判断,提高运行效率 即,第一次判断完后在内部返回一个新的同名函数,覆盖掉旧函数,之后再调用执行的就是新函数了
-
描述 在某些情况下,我们可能会遇到,只需要判断一次,例如从服务器拿某些数据,在页面生命周期内只需要拿一次,之后就直接用就行了,这种情况下就需要惰性函数
-
示例
// 简单的惰性函数
function getData () {
if (data) {
return function getData () {
console.log('用data渲染')
}
} else {
console.log('从服务器拿取数据,数据为 data')
}
}
高阶函数
-
满足以下条件之一的就是高阶函数
- 把函数作为参数
- 返回一个函数
-
作用 将函数作为参数,是运算的缺失,即我的函数只关注流程,不关心具体是如何实现的,而实现的缺失由作为参数的函数来补齐,即我的函数不执行具体工作,只提供执行工作的流程,下面将以简单的 map 函数实现为例 返回一个函数,是运算的延续,通过闭包的形式让我的函数中的东西能延续到下一个函数中,从而实现定制化的功能,换句话来说,就是定制化函数工厂
-
示例
// 示例一 简单的 map 函数
function myMap (fn) {
const arr = []
for(const i = 0; i < 原数组.length; i++) {
arr.push(fn)
// fn 每执行一次,就返回一个值为a
}
return arr
}
// 示例二 一个创建“乘法器”的工厂
const createMultiplier = (factor) => {
// 它返回了一个全新的、记住factor的函数
return (number) => number * factor;
};
const double = createMultiplier(2); // 工厂下线了“加倍器”
const triple = createMultiplier(3); // 工厂下线了“三倍器”
console.log(double(5)); // 10
console.log(triple(5)); // 15
将函数作为参数传递的特点
-
特点 函数延迟执行
-
示例
// 以下函数中,使用 () => xyz(5) 作为参数传递给了 add()
add(() => xyz(5))
// 相当于
const fn = () => xyz(5)
add(fn)
// 因此函数 fn 并没有被调用,只有在 add 内部调用时才会执行!
Proxy —— 代理
- 创建 代理接收两个参数,分别是目标对象和处理程序对象
// 创建一个简单的代理
const handler = {
// 分别为,目标对象,属性名,当前代理对象
get (target, property, receiver) {
// obj 属性被读取时触发
console.log(target === obj) // true
console.log(receiver === proxy) // true
return obj[property]
},
// 分别为,目标对象,属性名,新的值,当前代理对象
set (target, property, newValue, reciver) {
// obj 属性被修改时触发
obj[property] = newValue
}
}
// 接收两个参数,目标对象和处理程序对象
const proxy = new Proxy(obj, handler)
代理的特点
-
在代理对象上执行的所有操作都会无障碍地传播到目标对象
-
给目标对象或者代理对象赋值,会反映到两个对象上
- 给目标对象赋值,两个对象访问的是同一个值(即代理对象和目标对象的相同属性严格相等)
- 给代理对象赋值,代理对象会将赋值传递给目标对象
-
通过严格相等区别代理对象和目标对象
-
代理可以代理另一个代理 这意味着可以通过多层代理从而在一个目标对象上构建多层拦截网
-
作为语言的基础能力之一,在不支持代理的低版本中几乎无法被其他程序模拟和替代
const targetObj = { a: 1 }
const proxy = new Proxy(targetObj)
console.log(targetObj === proxy) // false
缺点
-
代理对象和目标对象的 this 指向问题 在目标对象中,this 指向目标对象,在代理对象中,this 指向代理对象,合乎常理但某些情况下(如代理某些实例)会出问题
-
代理与内部槽位 目前看不懂啊
可撤销代理
-
说明 对于使用 new Proxy() 创建的普通代理来说,代理对象和目标对象之间的联系在代理对象的生命周期内一直持续,如果有时候需要中断代理对象与目标对象之间的联系,就需要使用可撤销代理
-
返回值 返回一个包含了代理对象本身和它的撤销方法的可撤销
Proxy对象,具体为{"proxy": proxy, "revoke": revoke}
| 组成 | 含义 |
|---|---|
| proxy | 生成的代理对象,与 new Proxy() 生成的代理对象没有区别 |
| revoke | 撤销函数,没有参数,调用它会撤销和它一起生成的代理对象 |
- 语法
// 通过静态方法 revocable 可以创建一个可撤销的代理,proxy 是代理对象,revoke 可以撤销该代理
// 撤销函数和代理对象在实例化时同时生成
const { proxy, revoke } = Proxy.revocable(target, handler)
[!tips] 注意
- 撤销代理后再次调用代理(即使用 14 种可代理操作)会抛出错误 TypeError,其他操作不会抛出异常
- 撤销后不可恢复,再次调用 revoke() 不会有任何效果,也不会报错
捕获器(trap)
-
含义 在处理程序对象中定义的 基本操作的拦截器 ,每个处理程序对象中可以包含 0 个或多个捕获器,每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象前先调用捕获器函数,从而拦截并修改相应的行为
-
作用 可以通过各个代理器的参数去拦截并重建原始操作
[!tips] 注意
- 虽然可能有多个 JS 操作会触发同一个捕获器,但是,对于在代理对象上执行的任何一种操作,只会有一个捕获器被触发,不存在重复捕获的行为
捕获器不变式
- 含义 虽然捕获器几乎可以改变所有基本方法的行为,但是,捕获器处理程序也必须遵循”捕获器不变式“,用以防止捕获器定义出现过于反常的行为,捕获器的不变式因方法不同而异
捕获器 —— get()
-
触发条件 在获取属性值的操作中被调用,对应的反射API 为 Reflect.get()
-
拦截的操作
// 对象的 [] 方法
proxy[property]
// 对象的 . 方法
proxy.property
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
| property | 引用的目标对象上的字符串键属性 |
| receiver | 代理对象 |
-
返回值 无限制
-
语法
get (trapTarget, property, receiver) {
}
捕获器 —— set()
-
触发条件 在设置属性值的操作中被调用,对应的反射 API 为 Reflect.set()
-
拦截的操作
// 任何的赋值操作
proxy[property] = value
proxy.property = value
Object.create(proxy)[property] = value
// 对应的反射 API 触发时
Reflect.set(proxy, property, value, receiver)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
| property | 引用的目标对象上的字符串键属性 |
| value | 要赋给属性的值 |
| receiver | 代理对象 |
-
返回值 返回布尔值表示是否成功
-
语法
set (trapTarget, property, value, receiver) {
}
捕获器 —— has()
-
触发条件 在 in 操作符中被调用,对应的反射 API 为 Reflect.has()
-
拦截的操作
// in 操作符
property in proxy
property in Object.create(proxy)
// with 操作符
with (proxy) { { property } }
// 对应的反射 API 触发时
Reflect.has(proxy, property)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
| property | 引用的目标对象上的字符串键属性 |
-
返回值 返回布尔值表示是否拥有该属性,会发生隐式转换
-
语法
has (trapTarget, property) {
}
捕获器 —— defineProperty()
-
触发条件 在 Object.defineProperty() 中被调用,对应的反射 API 为 Reflect.defineProperty()
-
拦截的操作
Object.defineProperty(proxy, property, descriptor)
Reflect.defineProperty(proxy, property, descriptor)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
| property | 引用的目标对象上的字符串键属性 |
| description | 属性描述符,包括数据描述符(value, writable, enumerable, configurable)和存取描述符(get, set, enumerable, configurable) |
-
返回值 返回布尔值表示是否成功定义属性
-
语法
defineProperty (trapTarget, property, description) {
}
捕获器 —— getOwnPropertyDescriptor()
-
触发条件 在 Object.getOwnPropertyDescriptor() 中被调用,对应的反射 API 为 Reflect.getOwnPropertyDescriptor()
-
拦截的操作
Object.getOwnPropertyDescriptor(proxy, property)
Reflect.getOwnPropertyDescriptor(proxy, property)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
| property | 引用的目标对象上的字符串键属性 |
-
返回值 返回对象,如果属性不存在返回 undefined
-
语法
getOwnPropertyDescriptor (trapTarget, property) {
}
捕获器 —— deleteProperty()
-
触发条件 在 delete 操作符中被调用,对应的反射 API 为 Reflect.deleteProperty()
-
拦截的操作
// . 方法或 [] 方法删除属性
delete proxy.property
delete proxy[property]
// 对应的反射 API 触发时
Reflect.deleteProperty(proxy, property)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
| property | 引用的目标对象上的字符串键属性 |
-
返回值 返回布尔值,表示是否删除成功
-
语法
deleteProperty (trapTarget, property) {
return Reflect.deleteProperty(...arguments)
}
捕获器 —— ownKeys()
-
触发条件 在 Object.key() 及类似方法中被调用,对应的反射 API 为 Reflect.ownKeys()
-
拦截的操作
Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
Object.keys(proxy)
Reflect.ownKeys(proxy)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
| property | 引用的目标对象上的字符串键属性 |
-
返回值 返回包含字符串或符号的可枚举对象
-
语法
ownKeys (trapTarget) {
return Reflect.ownKeys(...arguments)
}
捕获器 —— getprototypeOf()
-
触发条件 Object.getprototypeOf() 中被调用,对应的反射 API 为 Reflect.getprototypeOf()
-
拦截的操作
Object.getprototypeOf(proxy)
proxy._proto_
Object.prototype.isPrototypeOf(proxy)
proxy instanceof Object
Reflect.getprototypeOf(proxy)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
-
返回值 返回对象或 null
-
语法
ownKeys (trapTarget) {
return Reflect.getprototypeOf(...arguments)
}
捕获器 —— setprototypeOf()
-
触发条件 Object.setprototypeOf() 中被调用,对应的反射 API 为 Reflect.setprototypeOf()
-
拦截的操作
Object.setprototypeOf(proxy)
Reflect.setprototypeOf(proxy)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
| property | 引用的目标对象上的字符串键属性 |
-
返回值 返回一个布尔值,表示原型赋值是否成功
-
语法
ownKeys (trapTarget, proterty) {
return Reflect.setprototypeOf(...arguments)
}
捕获器 —— isExtensible()
-
触发条件 Object.isExtensible() 中被调用,对应的反射 API 为 Reflect.isExtensible()
-
拦截的操作
Object.isExtensible(proxy)
Reflect.isExtensible(proxy)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
-
返回值 返回一个布尔值,表示 trapTarget 是否可扩展
-
语法
ownKeys (trapTarget) {
return Reflect.isExtensible(...arguments)
}
捕获器 —— preventExtensions()
-
触发条件 Object.preventExtensions() 中被调用,对应的反射 API 为 Reflect.preventExtensions()
-
拦截的操作
Object.preventExtensions(proxy)
Reflect.preventExtensions(proxy)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象 |
-
返回值 返回一个布尔值,表示 trapTarget 是否已经不可扩展
-
语法
ownKeys (trapTarget) {
return Reflect.preventExtensions(...arguments)
}
捕获器 —— apply()
-
触发条件 调用函数时被调用,对应的反射 API 为 Reflect.apply()
-
拦截的操作
proxy(...argumentsList)
Function.prototype.apply(thisArg, argumentsList)
Function.prototype.call(thisArg, ...argumentsList)
Reflect.apply(target, thisArg, argumentList)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标对象,要求必须是一个函数对象 |
| thisArg | 调用函数时的 this 参数 |
| argumentsList | 调用函数时的参数列表 |
-
返回值 无限制
-
语法
ownKeys (trapTarget, thisArg, argumentsList) {
return Reflect.apply(...arguments)
}
捕获器 —— construct()
-
触发条件 在 new 操作符中被调用,对应的反射 API 为 Reflect.construct()
-
拦截的操作
new proxy(...argumentsList)
Reflect.construct(target, argumentList, newTarget)
- 参数(按参数顺序排列)
| 参数 | 含义 |
|---|---|
| trapTarget | 目标构造函数 |
| argumentsList | 传递给目标构造函数的参数列表 |
| newTarget | 最初被调用的构造函数 |
-
返回值 必须返回一个对象
-
语法
ownKeys (trapTarget, argumentsList, newTarget) {
return Reflect.construct(...arguments)
}
代理模式
-
跟踪属性访问 通过 get set has 等操作可以直到对象属性是什么时候哦被访问、被查询的
-
隐藏属性 代理内部实现对外部代码是不可见的,可以利用代理构造一个不可见属性列表,遇到该属性就返回 undefined
-
属性验证 所有的赋值操作都会触发 set 捕获器,可以根据所赋的值决定允许还是拒绝赋值
-
函数与构造函数参数验证
Reflect —— 反射
- 含义 这是 JS 内置的对象,提供了一系列静态方法,这些方法可以直接调用内部方法从而执行对象的基本操作
[!tips] 注意 Relfeclt 不是函数对象,无法被构造,因此无法使用 new 操作符创建实例,它的方法都是静态方法
对象的基本操作
- 含义 只无法被分割为更细单元的对象操作,其他对象方法的实现都可以基于以下操作完成
| 基本操作 | 含义 | 说明 |
|---|---|---|
[[GetPrototypeOf]] | 获取隐式原型 | 确定提供继承属性的对象,null 表示无继承属性 |
[[SetPrototypeOf]] | 设置隐式原型 | 关联提供继承属性的对象,返回布尔值表示操作是否成功 |
[[IsExtensible]] | 检查对象是否可扩展 | 确定是否允许向该对象添加额外属性 |
[[PreventExtensions]] | 阻止对象扩展 | 控制是否可以向该对象添加新属性,返回操作是否成功 |
[[GetOwnProperty]] | 获取自有属性描述符 | 返回指定键的自有属性描述符,不存在则返回 undefined |
[[DefineOwnProperty]] | 定义自有属性 | 创建或修改自有属性,返回布尔值表示操作是否成功 |
[[HasProperty]] | 检查属性是否存在 | 返回布尔值表示对象是否拥有指定键的自有或继承属性 |
[[Get]] | 获取属性值 | 返回指定属性的值,Receiver 用作执行代码时的 this 值 |
[[Set]] | 设置属性值 | 设置指定属性的值,Receiver 用作执行代码时的 this 值,返回操作是否成功 |
[[Delete]] | 删除属性 | 移除指定键的自有属性,返回布尔值表示属性是否被删除或不存在 |
[[OwnPropertyKeys]] | 获取所有自有属性键 | 返回包含对象所有自有属性键的列表 |
函数特有的对象基本操作
| 基本操作 | 含义 | 说明 |
|---|---|---|
[[Call]] | 函数调用 | 执行与此对象关联的代码,通过函数调用表达式调用。参数包括 this 值和参数列表。实现此内部方法的对象是可调用的 |
[[Construct]] | 构造函数调用 | 创建一个对象,通过 new 运算符或 super 调用调用。第一个参数是构造函数调用或 super 调用的参数列表,第二个参数是 new 运算符最初应用的对象。实现此内部方法的对象称为构造函数 |
间接使用内部方法
-
以上基本操作并不暴露,属于 JS 的内部成员,我们无法直接调用,只能间接调用 (ES6 之前)
-
示例
const obj = {}
// 调用内部方法 [[Set]]
obj.a = 1
// 调用内部方法 [[Delet]]
delet boj.a
// 调用内部方法 [[SetPrototypeOf]]
Object.setPrototypeOf(obj, { a: 1 })
// 调用内部方法 [[GetPrototypeOf]]
Object.getPrototypeOf(obj, { a: 1 })
缺点
-
说明 间接调用内部方法完成操作时,会有一些其他的步骤,可能导致一些意料之外的结果
-
示例
const obj = { a: 1 }
// 定义不可写,不可枚举,不可配置的属性 b
Object.defineProperty(obj, 'b', {
value: 2,
enumerable: false
})
// 获取对象的键
const keys = Object.keys(obj)
// keys 方法会判断属性是否可枚举,如果可枚举才会加入到返回数组中,因此返回值为 ['a'] 属性 b 没有被添加到数组中,某些时候这可能不是我们期待的,我们可能需要的是所有属性,无论它是否可枚举
console.log(keys)
- 解决方法 直接调用内部方法,因此产生了能直接调用内部方法的需求,ES6时,为了解决上述问题,Reflect 应运而生
详解 Reflect
方法总览
| 方法名 | 作用 |
|---|---|
Reflect.getPrototypeOf(target) | 获取对象的原型 |
Reflect.setPrototypeOf(target, prototype) | 设置对象的原型 |
Reflect.isExtensible(target) | 判断对象是否可扩展 |
Reflect.preventExtensions(target) | 阻止对象扩展 |
Reflect.getOwnPropertyDescriptor(target, propertyKey) | 获取对象自有属性的描述符 |
Reflect.defineProperty(target, propertyKey, attributes) | 定义或修改对象自有属性 |
Reflect.has(target, propertyKey) | 检查对象是否拥有某个属性 |
Reflect.get(target, propertyKey[, receiver]) | 获取对象属性的值 |
Reflect.set(target, propertyKey, value[, receiver]) | 设置对象属性的值 |
Reflect.deleteProperty(target, propertyKey) | 删除对象的属性 |
Reflect.ownKeys(target) | 返回对象的所有自有属性键 |
Reflect.apply(target, thisArgument, argumentsList) | 调用函数 |
Reflect.construct(target, argumentsList[, newTarget]) | 调用构造函数创建实例 |
详细方法说明
Reflect.getPrototypeOf() —— 获取指定对象的隐式原型
-
返回值 对象的原型,如果没有继承属性则返回
null -
语法
Reflect.getPrototypeOf(target)
- 参数
| 参数 | 含义 |
|---|---|
| target | 要获取原型的对象 |
-
对应的内部方法:
[[GetPrototypeOf]] -
示例
const obj = {};
console.log(Reflect.getPrototypeOf(obj) === Object.prototype); // true
const arr = [];
console.log(Reflect.getPrototypeOf(arr) === Array.prototype); // true
Reflect.setPrototypeOf() —— 设置指定对象的原型
-
返回值
布尔值,表示设置是否成功 -
语法
Reflect.setPrototypeOf(target, prototype)
- 参数
| 参数 | 含义 |
|---|---|
| target | 要设置原型的对象 |
| prototype | 对象的新原型(对象或 null) |
-
对应的内部方法
[[SetPrototypeOf]] -
示例
const obj = {};
const proto = { foo: 'bar' };
const result = Reflect.setPrototypeOf(obj, proto);
console.log(result); // true
console.log(Reflect.getPrototypeOf(obj) === proto); // true
Reflect.isExtensible() —— 判断对象是否可扩展
-
返回值
布尔值,表示对象是否可扩展 -
语法
Reflect.isExtensible(target)
- 参数
| 参数 | 含义 |
|---|---|
| target | 要检查的对象 |
-
对应的内部方法
[[IsExtensible]] -
示例
const obj = {};
console.log(Reflect.isExtensible(obj)); // true
Reflect.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // false
Reflect.preventExtensions() —— 阻止对象扩展
-
返回值
布尔值,表示操作是否成功 -
语法
Reflect.preventExtensions(target)
- 参数
| 参数 | 含义 |
|---|---|
| target | 要阻止扩展的对象 |
-
对应的内部方法
[[PreventExtensions]] -
示例
const obj = { foo: 'bar' };
const result = Reflect.preventExtensions(obj);
console.log(result); // true
console.log(Reflect.isExtensible(obj)); // false
Reflect.getOwnPropertyDescriptor() —— 获取对象自有属性的描述符
-
返回值
属性描述符对象,不存在则返回undefined -
语法
Reflect.getOwnPropertyDescriptor(target, propertyKey)
- 参数
| 参数 | 含义 |
|---|---|
| target | 目标对象 |
| propertyKey | 属性键名 |
-
对应的内部方法
[[GetOwnProperty]] -
示例
const obj = { foo: 42 };
const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'foo');
console.log(descriptor);
// { value: 42, writable: true, enumerable: true, configurable: true }
Reflect.defineProperty() —— 定义或修改对象属性
-
返回值
布尔值,表示操作是否成功 -
语法
// descriptor 表示属性描述符
Reflect.defineProperty(target, propertyKey, descriptor)
- 参数
| 参数 | 含义 |
|---|---|
| target | 目标对象 |
| propertyKey | 属性键名 |
| descriptor | [[#Object.defineProperty —— 修改属性特性|属性描述符对象]] |
-
对应的内部方法
[[DefineOwnProperty]] -
示例
const obj = {};
const result = Reflect.defineProperty(obj, 'property', {
value: 42,
writable: false,
enumerable: true
});
console.log(result); // true
console.log(obj.property); // 42
Reflect.has() —— 检查对象是否拥有属性
-
返回值
布尔值,表示属性是否存在 -
语法
Reflect.has(target, propertyKey)
- 参数
| 参数 | 含义 |
|---|---|
| target | 目标对象 |
| propertyKey | 要检查的属性键 |
-
对应的内部方法
[[HasProperty]] -
示例
const obj = { foo: 42 };
console.log(Reflect.has(obj, 'foo')); // true
console.log(Reflect.has(obj, 'toString')); // true(继承属性)
Reflect.get() —— 获取对象属性的值
-
返回值
属性的值 -
语法
Reflect.get(target, propertyKey[, receiver])
- 参数
| 参数 | 含义 |
|---|---|
| target | 目标对象 |
| propertyKey | 属性键名 |
| receiver | getter 调用时的 this 值(可选) |
-
对应的内部方法
[[Get]] -
示例
const obj = { foo: 42 };
console.log(Reflect.get(obj, 'foo')); // 42
// 使用 getter 和 receiver
const obj2 = {
get foo() { return this.bar; },
bar: 43
};
console.log(Reflect.get(obj2, 'foo', { bar: 44 })); // 44
Reflect.set() —— 设置对象属性的值
-
返回值
布尔值,表示设置是否成功 -
语法
Reflect.set(target, propertyKey, value[, receiver])
- 参数
| 参数 | 含义 |
|---|---|
| target | 目标对象 |
| propertyKey | 属性键名 |
| value | 要设置的值 |
| receiver | setter 调用时的 this 值(可选) |
-
对应的内部方法
[[Set]] -
示例
const obj = {};
Reflect.set(obj, 'foo', 42);
console.log(obj.foo); // 42
// 使用 setter
const obj2 = {
set foo(value) { this.bar = value; }
};
Reflect.set(obj2, 'foo', 43, obj2);
console.log(obj2.bar); // 43
Reflect.deleteProperty() —— 删除对象的属性
-
返回值
布尔值,表示删除是否成功 -
语法
Reflect.deleteProperty(target, propertyKey)
- 参数
| 参数 | 含义 |
|---|---|
| target | 目标对象 |
| propertyKey | 要删除的属性键 |
-
对应的内部方法
[[Delete]] -
示例
const obj = { foo: 42, bar: 43 };
Reflect.deleteProperty(obj, 'foo');
console.log(obj); // { bar: 43 }
Reflect.ownKeys() —— 返回对象的所有自有属性键
-
返回值
包含所有自有属性键的数组 -
语法
Reflect.ownKeys(target)
- 参数
| 参数 | 含义 |
|---|---|
| target | 目标对象 |
-
对应的内部方法
[[OwnPropertyKeys]] -
示例
const obj = {
[Symbol('id')]: 123,
enumKey: 'value'
};
Object.defineProperty(obj, 'nonEnumKey', {
value: 'hidden',
enumerable: false
});
console.log(Reflect.ownKeys(obj));
// ['enumKey', 'nonEnumKey', Symbol(id)]
Reflect.apply() —— 调用函数
-
返回值
函数调用的结果 -
语法
Reflect.apply(target, thisArgument, argumentsList)
- 参数
| 参数 | 含义 |
|---|---|
| target | 要调用的函数 |
| thisArgument | 函数调用时的 this 值 |
| argumentsList | 参数列表(类数组对象) |
-
对应的内部方法
[[Call]] -
示例
function sum(a, b) {
return a + b;
}
console.log(Reflect.apply(sum, null, [1, 2])); // 3
// 调用内置方法
console.log(Reflect.apply(String.prototype.trim, ' hello ', [])); // 'hello'
Reflect.construct() —— 调用构造函数
-
返回值
构造的新对象 -
语法
Reflect.construct(target, argumentsList[, newTarget])
- 参数
| 参数 | 含义 |
|---|---|
| target | 要调用的构造函数 |
| argumentsList | 构造函数参数列表 |
| newTarget | 用作 new.target 的值(可选) |
-
对应的内部方法
[[Construct]] -
示例
function Person(name) {
this.name = name;
}
const person = Reflect.construct(Person, ['Alice']);
console.log(person.name); // 'Alice'
// 使用不同的 newTarget
function Animal() {}
const person2 = Reflect.construct(Person, ['Bob'], Animal);
console.log(person2 instanceof Animal); // true
模块化
-
含义 在 Node.js 中,每个文件都被视为一个单独的模块
-
优势 提高代码复用性,按需加载,独立作用域
-
使用 需要标准语法导出和导入使用
[!tips] 注意
- 在Node.js项目环境中一般使用 CommonJS标准
- 在前端工程化项目中一般使用 ECMScript标准
CommonJS 标准语法——导出和导入
- 导出
moudle.exports = {}
- 导入
require('模块名或路径')
[!tips] 注意
- 内置模块:直接写模块名(如, fs,path,http),node.js 自带的和npm下载的包可以直接写模块名
- 自定义模块:写模块文件路径(如,./utils.js)
- 示例
const baseURL = 'http://hmajax.itheima.net'
const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
// 导出
module.exports = {
url: baseURL,
arraySum: getArraySum
}
// 导入
const obj = require('./utils.js')
console.log(obj)
// {
// url: baseURL,
// arraySum: getArraySum
// }
ECMScript 标准——默认导出和导入
- 导出
export default{}
- 导入
import 变量名 from '模块名或路径'
[!tips] 注意
- 这种方式是静态导入,是同步的,根据依赖关系先后加载 js 文件(如果造成了循环依赖可能会导入失败)
- 示例——集中导出 通过 export default {} 集中导出,需要自己将键值对写进对象中
const baseURL = 'http://hmajax.itheima.net'
const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
// 导出
export default {
url: baseURL,
arraySum: getArraySum
}
// 导入
import obj from './utils'
console.log(obj)
// {
// url: baseURL,
// arraySum: getArraySum
// }
- 示例——命名导出 在定义语句前添加 export 关键字,被添加关键字的定义语句将被导出
// 导出
export const baseURL = 'http://hmajax.itheima.net'
export const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
// 导入
import {baseURL, getArraySum} from '文件路径'
[!tips] 注意
- Node.js 默认支持 CommonJS 标准语法
- 如果需要使用 ECMScript 标准语法,在运行模块所在文件夹新建 package.json 文件,并设置{ "type": "module" }
- 新版的 Node.js 已经直接支持 ECMScript 标准语法,无需配置文件
- 命名导出和集中导出两种方式不能混在一起用
重新导出
- 语法
export * from 路径
- 示例
export * from './modules/user'
// 以上语法相当于
// ./modules/user.js
export const getUser = () => { /* ... */ }
export const createUser = () => { /* ... */ }
export const updateUser = () => { /* ... */ }
export const deleteUser = () => { /* ... */ }
// + 下面的内容
export { getUser, createUser, updateUser, deleteUser } from './modules/user'
动态导入
[!tip] 注意
- 动态导入通常用在函数等地方(总之不是在文件的开头),它是异步的,返回一个 promise
- 语法
// 对于命名导出
import('./main.js').then(({ pinia }) => {
// 这是异步的,返回 Promise
})
// 对于默认导出(export default)
import(路径).then((pinia) => {})
深入模块化
单例性
-
描述 在几乎所有编程语言中,模块都是单例的,在应用运行时,遇到首次加载的模块,会运行它,并将运行结果缓存,再次加载模块时,会直接使用缓存
-
实现方式 通过缓存对象缓存模块结果,缓存对象在不同语言的表现不同
-
优点 一、性能 模块只加载一次,性能好
二、数据一致性 引入模块的地方,能共享模块状态,从而保证了数据的一致性,不同模块间能够相互协作
事件循环
[!tip] 注意
- 单线程是异步产生的原因
- 事件循环是异步的实现方式
- 可以使用厨房和工作人员来形容进程和线程
进程
- 内容 简单来说,程序运行需要有其专属的内存空间,这块内存空间就是进程 进程时资源分配的最小单位,进程是操作系统进行资源分配和保护的基本单位
[!tip] 注意
- 每个应用至少有一个进程,进程之间相互独立,如果不同进程同意,则可以进行通信
线程
- 描述 有了进程(内存空间之后),就可以运行程序代码了,运行代码的 “人” 称之为线程 线程是 CPU 调度的最小单位,而线程是程序执行和调度的基本单
[!tip] 注意
- 一个进程至少有一个线程,在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程
- 如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,因此一个进程中可以包含多个线程
区别
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 根本性质 | 资源分配的单位。拥有独立的地址空间和系统资源。 | CPU调度和执行的单位。共享进程的资源,是进程内的一条独立执行流。 |
| 资源开销 | 大。创建、切换、销毁需分配/回收内存、I/O等资源,成本高。 | 小。共享进程资源,主要维护程序计数器、寄存器、栈等私有数据,创建切换快。 |
| 内存空间 | 拥有独立的虚拟地址空间,相互隔离。 | 共享所属进程的地址空间和全局变量,通信简便但需同步机制。 |
| 通信机制 | 复杂。需进程间通信机制,如管道、消息队列、共享内存。 | 简单。可直接读写共享的进程数据(需同步)。 |
| 独立性 | 高。一个进程崩溃通常不影响其他进程。 | 低。一个线程崩溃可能导致整个进程终止;线程间会相互影响。 |
| 创建与切换 | 开销大,速度慢,涉及资源管理。 | 开销小,速度快,主要在CPU寄存器层面。 |
浏览器的进程和线程
- 描述 浏览器是一个多进程多线程的应用程序(浏览器太复杂了,如果只开启一个进程,占用一块内存空间,在发生错误时容易造成连环崩溃的概率,因此需要将各种功能放置到不同的内存空间中)
[!tip] 注意
- 每个标签页都有一个渲染进程
- 三个主要的进程 (跟前端有关的) 主要为浏览器进程、网络进程、渲染进程
![[z_attachments/334e6091a73e53b2dcd440601533ff22.jpg|500]]
- 浏览器进程和网络进程
![[z_attachments/Pasted image 20251112223829.png|500]]
- **渲染进程“
![[z_attachments/Pasted image 20251112223946.png|500]]
- 在浏览器中查看进程
![[z_attachments/Pasted image 20251112223555.png| 500]]
浏览器进程(主进程)
-
作用 主要负责界面显示(负责界面如下)、用户交互(点击鼠标,滚轮,键盘等)、子进程管理(其他进程都由浏览器启动)等。浏览器进程内部会启动多个线程处理不同的任务
-
这是浏览器进程负责的界面显示
![[z_attachments/Pasted image 20251112224441.png]]
网络进程
- 作用 负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务
⭐渲染进程⭐
- 作用 渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML、CSS、JS 代码。 默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响
渲染主线程
-
描述 渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 每秒渲染60帧
- 执行全局 JS 代码
- 执行事件处理函数
- 执行计时器的回调函数
- ......
-
为什么不将上述功能拆分成多个线程?
- 上述功能相互影响,无法完全分割
- 很多操作是原子级的,要求有先后顺序,多线程竞争可能无法保证顺序
事件循环
-
描述 在运行代码时,会遇到如定时器,计时器,回调函数到了执行的时候,面对一个问题,是停止当前代码去运行这些回调,还是等代码运行完再执行上述操作
-
解决方案 —— 排队(事件循环)
- 在最开始的时候,渲染主线程会进入一个无限循环(死循环)
- 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的未尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
![[z_attachments/Pasted image 20251112230430.png|600]]
异步
-
问题 代码运行过程中,会遇到一些无法立即处理的任务(计时器,网络通信,用户操作后执行的任务),若渲染主线程等待这些任务的时机到达,会导致主线程长期处于”阻塞“状态,从而导致浏览器卡死
-
同步示意图
![[z_attachments/23a2f2f57ab4d06569b0fe1319e3d58a.jpg]]
- 异步示意图 由于渲染主线程承担着复杂而重要的任务,不能等待上面的命令完成再执行因此使用了异步的模式,优点是主线程永远不会阻塞
![[z_attachments/876747ee398b36eb0a1f7c29446fad2d.jpg]]
- 为什么 JS 需要异步 JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。 所以浏览器采用异步的方式来避免。县体做法是当某些任务发生时,比如计时器、网络、事件监听、主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务对象,加入到消息队列的末尾排队,等待主线程调度执行。 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
JS阻碍渲染示例 —— 深入理解事件循环
<body>
<h1>廖成林</h1>
<button>点击</button>
<script>
const h1 = document.querySelector('h1')
const btn = document.querySelector('button')
// 延迟运行函数
function delay (duration) {
const start = Date.now()
while (Date.now() - start < duration) {
// 空循环
}
}
btn.addEventListener('click', () => {
h1.textContent = '丁换换'
delay(3000)
})
</script>
</body>
-
描述 在上面的代码中,渲染主线程首先执行一遍代码,将 btn 的点击监听交给交互线程,然后主线程休眠,当用户点击按钮后,交互线程会将任务对象放入消息队列,渲染主线程被唤醒执行任务(即执行回调函数),而任务第一行改变了 h1 的文本,它产生了一个新任务(将改变后的文本绘制到页面上),这个新任务被推进消息队列,然后执行第二行,结果遇到了一个三秒的死循环,这时候主线程做不了其他任何事情(页面卡死,新的界面不被渲染),直到循环结束后,主线程没有任何任务需要执行,再去消息队列中取出将新文本绘制到页面上的任务并完成,页面才更新
-
结果 点击按钮后,页面卡死了3秒,然后才更新文本
任务与消息队列的优先级
-
描述 任务没有优先级,在消息队列中先进先出,但是消息队列具有优先级
-
W3C 解释
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同队列
- 在一次事件循环中,浏览器可以根据实际情况从不同的任务队列中取出任务执行
- 浏览器必须准备好一个微队列,微队列中的任务优先于其他所有任务执行(甚至于比绘制页面的队列还高)
- 解释的原文链接
-
目前的 chrome 实现中,至少包括了以下队列 延时队列 —— 用于存放计时器到达后的回调任务,优先级「中」 交互队列 —— 用于存放用户操作后产生的事件处理任务,优先级「高」 微队列 —— 用户存放需要最快执行的任务,优先级「最高」
[!tip] 注意
- 在浏览器中,交互队列的优先级一般都很高,因为如果
阐述事件循环
事件循环又叫做消息循环(官方描述 event loop,浏览器源码(内部实现)为message loop),是浏览器渲染主线程的工作方式, 在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列未尾即可。 过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。 根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行
JS 计时器能否精确计时,为什么
不行,因为:
-
计算机硬件没有原子钟,无法做到精确计时
-
操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这 些偏差
-
按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过5 层,则会带有 4 毫秒的最少时间,这样 在计时时间少于 4毫秒时又带来了偏差(例如 settimeout 中嵌套 ettimeout,嵌套大于等于五层时,每层都设定0秒,那么从最外层的 setTimeout 开始算,第六层及以后时间参数如果小于 4 毫秒,会被设定为 4 毫秒)
-
受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差
浏览器渲染原理
- 渲染原理图
![[z_attachments/Pasted image 20251113134156.png|600]]
- 渲染流水线
![[z_attachments/Pasted image 20251113134350.png|600]]
render —— 渲染
- 描述 对于浏览器而言,渲染就是将 html 字符串(即网页代码)变成像素信息的过程
步骤一:解析HTML —— Parse HTML
[!tip] 注意 DOM 和 CSSOM 树都是对象
DOM 树 —— Document Object Model
![[z_attachments/Pasted image 20251113134945.png|600]]
CSSOM 树 —— CSS Object Model
- 通过 JS 访问
// 可以访问 CSSOM 树,每条 CSSStyleRule 包括选择器和 style ,如下图
document.styleSheets
// 示例 —— 通过 CSSOM 树给所有div元素添加边框
document.styleSheets[0].addRule('div', 'border: 2px solid #333 !important')
![[z_attachments/Pasted image 20251113135334.png|600]]
解析 HTML 时遇到 CSS 代码时如何处理
-
描述 代码从上到下开始解析,可能会在 HTML 解析完成前遇到 style 标签或 外部引入的 css 文件
-
解决方案 为了提高解析效率,浏览器会启动一个预解析器率先下载和解析 CSS
-
详细
在网络线程下载 HTML 字符串后,形成一个渲染任务,它会将渲染任务推入消息队列,渲染主线程从消息队列中取出渲染任务,开始解析 HTML
与此同时,预解析线程快速扫描整个 HTML ,提前发现外部资源(link , img 等)并立即通知网络线程开始下载CSS文件,这个过程不阻塞渲染主线程,网络线程可以并发下载多个 CSS 文件(外部资源,其中 CSS 资源优先级高于图片等外部资源),并且检查缓存,避免重复下载
渲染主线程解析 HTML 时并不会等待 CSS 下载完成,只有遇到内部 CSS 样式时(style 标签或内联 css)才会同步解析,将解析结果添加到 CSSOM 树中
在解析 HTML 的过程中,如果 CSS 文件下载完成,主线程会暂停 HTML 解析,立即解析这些 CSS 文件构建完整的 CSSOM 树,如果 HTML 解析完成(已构建 DOM 树),主线程则会同步暂停(期间处理其他任务,只有在首次渲染前会等待),等待 CSS 文件下载完成后,解析 CSS 形成 CSSOM 树,最后 DOM 树和 CSSOM 树全部生成完毕后会产生渲染树
![[z_attachments/Pasted image 20251113140615.png|600]]
[!tip] 注意
- 图中预解析线程解析 CSS 的说法是错误的,所有 CSS 都在渲染主线程中解析,因为 CSS 解析需要考虑层叠规则和继承等复杂逻辑,这些逻辑都要与 DOM 树构建协调进行
三个线程的任务
-
预解析线程 在渲染主线程开始工作的同时,快速浏览一遍 HTML ,遇到 link img 等外部资源后将下载任务分发给网络线程,网络线程并发下载这些外部资源
-
网络线程 在接收到下载任务后并发下载外部资源,优先下载 CSS 资源,下载完成后直接将 CSS 文件推给主线程
-
渲染主线程 解析HTML,遇到内部 CSS(style 和内联css)时同步解析 CSS 构建 CSSOM 树,遇到 script 标签时暂停,等待 JS 文件下载,下载完并将 JS 代码解析执行完成后,继续解析 HTML ,如果遇到网络线程推过来的 CSS 文件也暂停解析 HTML ,优先解析 CSS ,如果是页面首次渲染,在 DOM 树构建完成后且CSS还未下载完成,则会暂停等待 CSS 代码下载,下载完成构建 DCCOM 树后,再合成渲染树
解析 HTML 时遇到 JS 时如何处理
- 处理 遇到 JS 代码时会暂停解析,等待 JS 下载完成,执行完 JS 后才继续解析 HTML ,因为 JS 代码有可能修改 DOM 树,因此必须要等待 JS
![[z_attachments/Pasted image 20251113150033.png|600]]
步骤二:样式计算 —— Recalculate Style
- 描述 主线程会遍历得到的 DOM 树,依次为树中的每个节点进行 CSS 属性计算,计算出它最终的样式,称之为 Computed Style。在这一过程中,很多预设值会变成绝对值,比如red 会变成rgb(255,0,0);相对单位会变成绝对单位,比如 em 会变成 px 这一步完成后,会得到一棵带有样式的 DOM 树
[!tip] 注意
- 绝大部分预设值和相对值在此步骤被转化为绝对值,但是百分比单位不会,因为它的值的确定与包含块有关,需要在布局阶段确立
- 具体的样式计算链接 [[1_计算机/前端/三件套/CSS#CSS 属性计算|CSS 属性计算]]
![[z_attachments/Pasted image 20251113150512.png|600]]
步骤三:布局 —— Layout
[!tip] 注意
- 布局树中的每个节点都是对象,但它不是 DOM 对象,是 C++ 的对象,block flex grid 等不同的盒子有不同的 C++ 类来实现,都是 C++ 对象
- 布局树会暴露部分信息,如 clientWidth , offsetWidth 等,通过 JS 获取后,它们都只有数值,没有单位
- 描述 布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息(即使用值)。例如节点的宽高、相对包含块的位置。大部分时候,DOM 树和布局树并非-一对应,如下方 display: none; 的节点没有几何信息并不会进入布局树,而 ::before 等虽然不在 DOM 树中,但会出现在布局树中(因为它们有几何信息)
[!tip] 注意 3. 在这个步骤中,百分比值最终会根据包含块的值被转换为绝对单位,称为使用值,通过 offsetWidth, clientWidth 等就是使用值,至此,所有 css 相对单位都被转换为了绝对单位 4. 区分使用值和计算值得方法为,通过
window.getComputedStyle(element)获得得为计算值,计算值是字符串,使用值是数值(不带单位) 5. 在现代浏览器中,获取计算值时,获取到得百分比值有可能变成了绝对单位,这取决于浏览器得实现,但是上述得步骤是完全正确的,获得了绝对单位并不是因为在样式计算那一步就把百分比变成了绝对单位,而是浏览器为了用户更方便调试和使用而实现的
![[z_attachments/Pasted image 20251113152237.png|600]]
- 示例 p 是块盒,而内容 a 直接包含在 p 中,会导致在布局树中,在将 a 包裹在一个匿名行盒中,而内容 b 跟两个块盒相邻,则它会被一个匿名块盒包裹,匿名块盒中再添加一个匿名行盒,b 在行盒中
[!tip] 注意
- 内容必须在行盒中
- 行盒和块盒不能相邻
![[z_attachments/Pasted image 20251113154200.png|600]]
步骤四:分层 —— Layer
[!tip] 注意
- 分层策略是由浏览器内部决定的,我们无法直接决定分层
- 滚动条会单独分层
- 跟堆叠上下文有关的属性会影响分层,如 z-index , opacity , transform 等
- 分层越多,内存消耗越大
- 图层控制台 在 chrome 浏览器可以打开分层控制台(edge 浏览器没找到,好像没有),打开方式和图层控制台情况如下图所示
![[z_attachments/Pasted image 20251113160534.png|600]]
will-change —— 告知浏览器,希望为某个元素单独分层
- will-change 链接 [[1_计算机/前端/三件套/CSS#will-change —— 告诉浏览器元素将要变化|will-change —— 告诉浏览器元素将要变化]]
[!tip] 注意
- 这个属性是对分层影响最大的属性,因为它明确告知浏览器希望能对该元素单独分层
步骤五:绘制 —— Paint
- 描述 这里的绘制,是指为每一层生成如何绘制的指令(这里的指令类似于,将画笔移动到某位置,绘制一个矩形(或别的),填充红色(或别的)等等),canvas 的绘制就是调用了浏览器内部的绘制方法
![[z_attachments/Pasted image 20251113162050.png|600]]
![[z_attachments/Pasted image 20251113162235.png|600]]
步骤六:分块 —— Tiling
- 描述 分块(块为 tile )会将每一层分为多个小的区域 ,优先绘制视口区域的内容
![[z_attachments/Pasted image 20251113162507.png|600]]
- 分块工作交给多个线程同时进行
![[z_attachments/Pasted image 20251113162612.png|600]]
- mac 通过活动监视器查看分块
![[z_attachments/Pasted image 20251113163131.png|600]]
步骤七:光栅化 —— Raster
-
描述 光栅化是将每个块变成位图,它优先处理靠近视口的块,合成线程会将工作交给 GPU 进程,让它完成光栅化,并将位图返回给合成线程
-
示例 会优先绘制 bcef 块
![[z_attachments/Pasted image 20251113163743.png|600]]
- 此过程会使用到 GPU 加速
![[z_attachments/Pasted image 20251113163904.png|600]]
步骤八:画 —— Draw
[!tip] 注意
- 在这个过程中,合成线程并没有直接把信息交给硬件绘制,而是交给 GPU 进程,由 GPU 交给硬件绘制,这是因为合成线程位于渲染进程,而渲染进程处在沙盒环境中(跟操作系统硬件隔离开,能保证安全,在浏览器中遭到攻击时,只会影响浏览器,而不会危害计算机安全)
- transform 的旋转变形等在这一步才实现
-
描述 合成线程会计算出每个位图在屏幕上的位置,交给 GPU 进行最终呈现
-
quad 指引信息,包括位图距离屏幕的位置,优先绘制谁等等
![[z_attachments/Pasted image 20251113164913.png|600]]
⭐完整渲染过程⭐
当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。 在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启染流程。
整个渲染流程分为多个阶段,分别是: HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。这样,整个渲染流程就形成了一套组织严密的生产流水线。
渲染的第一步是解析 HTML。 解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML中的外部 CSS 文件和 外部的 JS 文件。
如果主线程解析到 link 位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的HTML。当浏览器下载完 CSS 文件时,主线会暂停解析 HTML ,先将 CSS 解析,并构建 CSSOM 树,然后继续解析 HTML
如果主线程解析到 script 位置(遇到JS会暂停解析),会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻寨 HTML 解析的根本原因。
第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在CSSOM 树中。
渲染的下一步是样式计算。
主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。在这一过程中,很多预设值会变成绝对值,比如red 会变成rgb(255,0,0);相对单位会变成绝对单位,比如 em 会变成 px
这一步完成后,会得到一棵带有样式的 DOM 树。
接下来是布局,布局完成后会得到布局树。
布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。 大部分时候,DOM 树和布局树并非-一对应。
比如 display:none 的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法-一对应
下一步是分层
主线程会使用一套复杂的策略对整个布局树中进行分层,分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。
滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过 will-change 属性更大程度地影响分层结果
再下一步是绘制
主线程会为每个图层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成合成线程首先对每个图层进行分块,将其划分为更多的小区域。
合成线程会从线程池(线程管理器)中拿取多个线程(分块器)来完成分块工作
分块完成后,进入光栅化阶段
合成线程会将块信息交给 GPU进程,以极高的速度完成光栅化。
GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。
光栅化的结果,就是一块一块的位图
最后一个阶段就是画了
合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息
指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。
变形发生在合成线程,与渲染主线程无关,这就是 transform 效率高的本质原因(这也是为什么 transform 不会影响布局,因为它的步骤位于布局步骤之后)
合成线程会把 quad 提交给 GPU进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像
reflow —— 重排
- reflow 的影响 通过 JS 修改 DOM 或 CSSOM 树,会导致从样式或者布局开始发生变化,后续步骤需要重新进行一遍,绘制新画面,这就是重排,重排的开销较大,应避免频繁重排(冲突或模糊处以解释内容为准)
![[z_attachments/Pasted image 20251113165428.png|600]]
解释
reflow 的本质就是重新计算 layout 树
当进行了会影响布局树的操作后,需要重新计算布局树,会引发layout
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的(影响布局后,会将新的渲染任务推入消息队列,等JS同步执行完后再从队列中拿取任务)
也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息
浏览器在反复权衡下,最终决定获取几何信息属性立即 reflow(获取几何信息属性信息时直接触发 reflow 并且该 reflow 是同步的,JS 会暂停等待 reflow)
repaint —— 重绘
解释
repaint 的本质就是重新根据分层信息计算了绘制指令
当改动了可见样式后,就需要重新计算,会引发 repaint
由于元素的布局信息也属于可见样式,所以reflow 一定会引起 repaint
说明
-
如果改变的可见样式没有影响分层,则从 paint 步骤开始(红色框)
-
如果影响了分层,则从 layout 步骤开始(蓝色框)
![[z_attachments/Pasted image 20251113170347.png|600]]
为什么 transform 效率高
解释
说明
- 直接在 CSSOM 中改动 transform 使用 JS 改变 transform ,虽然改变了第二步 style ,但是由于 transform 跟前面的步骤无关,后面的步骤会跳过,一直到第八步(Draw)才会执行改变 transform 的操作,非常快(如果遇到死循环,它也会被堵塞,因为死循环在它前面,它还没被执行到,当死循环结束后才轮到该行 js 代码)
![[z_attachments/Pasted image 20251113171022.png|600]]
- 使用动画改动 transform 使用 animation 改变 transform ,由于没有改变 CSSOM ,直接在最后一步(Draw)变化,更快(而且不会被死循环阻塞)
![[z_attachments/Pasted image 20251113171226.png|600]]
- 示例 在下面的例子中,点击按钮后,页面会卡死,而通过改动 left 的 ball1 的动画会被阻塞(left 会触发重排,需要主线程工作),改动 transform 的动画继续运行(不通过主线程)
<style>
.ball1,
.ball2 {
width: 100px;
height: 100px;
border-radius: 50%;
margin: 100px;
}
.ball2 {
position: absolute;
top: 400px;
left: 0;
}
.ball1 {
background-color: red;
animation: ball1 1s alternate infinite ease-in-out;
}
.ball2 {
background-color: blue;
animation: ball2 1s alternate infinite ease-in-out;
}
@keyframes ball1 {
to {
transform: translateX(300px);
}
}
@keyframes ball2 {
to {
left: 300px;
}
}
</style>
</head>
<body>
<button>点击</button>
<div class="ball1"></div>
<div class="ball2"></div>
<script>
const btn = document.querySelector('button')
function delay (duration) {
const start = Date.now()
while (Date.now() - start < duration) {
// 空循环
}
}
btn.addEventListener('click', () => {
delay(5000)
})
</script>
</body>
</html>
读取元素尺寸集合
[!tip] 注意
- el.style.width 等如果未设置读出来为 '' ,而 getComputedStyle(el).width 等未设置也会读出一个具体的值(某个数字 + px)(样式计算会把所有属性都计算出具体值)
- 部分具体样式详情可以参考 [[#元素尺寸与位置]]
| 方式 | 含义 | 优缺点 | 来源 |
|---|---|---|---|
| el.style.width/height | 读取 style 属性填写的宽高 | 不回流、重绘,性能高, 但读出来的不一定是实际尺寸 | DOM树、CSSOM树 |
| getComuptedStyle(el) | 读取计算后的样式 | 读出来的宽高 不包括 border 和 padding 读取值为数字+单位的字符串 读取值是被近似过的 | [[#步骤二:样式计算 —— Recalculate Style|浏览器渲染步骤——样式计算]] |
| offsetWidth clientWidth scrollWidth | 读取布局树中的信息 元素在页面上的布局信息 | 无单位 | [[#步骤三:布局 —— Layout|浏览器渲染步骤——布局]] |
| el.getBoundingClientRect() | 视觉尺寸 | 会读取被 transform 等属性影响后的尺寸 如 scale(2) 会读取放大两倍的尺寸 读取的是浮点值 | [[#步骤八:画 —— Draw|浏览器渲染步骤——画]] |
off 前缀 —— 偏移尺寸(offset dimensions)
- 描述 包含元素在屏幕上占用的所有视觉空间,元素在页面上的视觉空间由其 border box 决定(因为这些都能设置颜色等让我们看见)
![[z_attachments/Pasted image 20251130003435.png| 400]]
- offsetParent 规则
- 有定位祖先,最近的
position: relative/absolute/fixed祖先元素的 padding box - 表格元素相对于最近的 table
- 无定位祖先,相对于 body
position: fixed或display: none的元素,offsetParent 为null
- 有定位祖先,最近的
client 前缀 —— 客户端尺寸(client dimensions)
- 描述 padding box 也称为客户端区域,包括 clientWidth 和 clientHeight
![[z_attachments/Pasted image 20251130004139.png|400]]
scroll 前缀 —— 滚动尺寸(scroll dimensions)
[!tip] 注意
- scrollWidth , scrollHeight 表示内容大小,指的是整体内容的大小(即下图的整个隐藏区域 如果有滚动条则表示除去滚动条的内容区域大小),相对于 padding box
- scrollTop , scrollLeft 被隐藏内容的宽高(即元素可见区域顶部至元素内容区域顶部的距离,左侧同理)
- 描述 滚动尺寸提供了元素内容滚动距离的信息
![[z_attachments/Pasted image 20251130004756.png|400]]
- 作用 scrollLeft 和 scrollTop 可写,用于指定当前元素滚动的位置
el.getBoundingClientRect() —— 相对视口位置
[!tip] 注意
- 该方法读取的元素尺寸受 transform 影响
- 相对于 border box
- 注意元素尺寸大于视口时,返回的值虽然含义没变,但实际作用却变了(细品)
- 描述 返回一个 DOMRect 对象,包括 left , right , bottom , height , width 属性,对应浏览器渲染管线——画
![[z_attachments/Pasted image 20251130005807.png|400]]
导致重排的属性/方法
| 类别 | 属性/方法 | 为什么会导致重排? |
|---|---|---|
| Offset (偏移) | offsetWidthoffsetHeightoffsetLeftoffsetTopoffsetParent | 需要计算元素在文档流中的确切几何位置 |
| Scroll (滚动) | scrollTopscrollLeftscrollWidthscrollHeight | 需要知道完整内容的尺寸和当前的滚动位置 |
| Client (客户端) | clientWidthclientHeightclientTopclientLeft | 需要计算 Padding Box 的尺寸(不含滚动条) |
| 方法 | el.getBoundingClientRect()el.getClientRects() | 需要计算视口中的精确位置,且包含 Transform 变换,必须基于最新的布局树 |
| 样式 | getComputedStyle(el) | 虽然主要触发"样式计算",但如果读取了涉及布局的属性(如宽高、位置),也会触发重排 |
| 文本 | el.innerText | 需要计算样式来决定哪些文本是隐藏的(例如 display: none 的文本不会被读出来) |
不会导致重排的属性
| 方式 | 属性 | 说明 |
|---|---|---|
| Style 对象 | el.style.widthel.style.heightel.style.top... | 读取这些属性不会导致重排,因为它们只是直接读取 DOM 节点上 style 属性的字符串值,不需要经过渲染管线计算 |
| 文本内容 | el.textContent | 读取原始文本节点内容,不关心样式和布局,因此开销极小 |
重要注意事项
textContentvsinnerText: 读取textContent不会触发重排,而innerText会触发重排getComputedStyle: 读取非布局相关属性(如color)不会触发重排,但读取布局属性(如width)会触发重排- 样式读取优化: 批量读取布局属性,避免"读取-修改-读取"的交替模式
表格 1:浏览器宽度属性对比
| 属性 / 特性 | 描述 | 是否包含浏览器UI (如地址栏/边框) | 是否受页面缩放影响 (Ctrl + +/-) | 是否受捏合缩放影响 (Pinch Zoom) | 主要用途 |
|---|---|---|---|---|---|
| window.outerWidth | 整个浏览器窗口的宽度,包括侧边栏、边框。 | ✅ 包含 | ❌ 不影响 | ❌ 不影响 | 获取浏览器窗口本身的物理尺寸。 |
| window.innerWidth | 布局视口 的宽度。即可见页面区域,包含滚动条。 | ❌ 不包含 (地址栏等) | ✅ 影响 (值变小) | ❌ 不影响 | 响应式布局 (CSS Media Queries 基于此)。 |
| visualViewport.width | 视觉视口 的宽度。用户当前实际看到的区域。 | ❌ 不包含 | ✅ 影响 | ✅ 影响 | 移动端处理键盘弹起、双指缩放后的精细布局。 |
表格 2:详细属性速查表
| 类别 | 属性 | 描述 | 是否只读 | 触发重排 | 包含滚动条 | 受缩放影响 (Ctrl+) | 主要用途 |
|---|---|---|---|---|---|---|---|
| 滚动位置 | window.scrollXwindow.pageXOffset | 文档水平已滚动的距离 | ✅ | ❌ (通常) | - | ✅ | 获取/设置滚动位置 |
| 滚动位置 | window.scrollYwindow.pageYOffset | 文档垂直已滚动的距离 | ✅ | ❌ (通常) | - | ✅ (CSS像素值) | 获取/设置滚动位置 |
| 视口尺寸 | window.innerWidth | 布局视口 宽度 | ✅ | ⚠️ (极低) | ✅ | ✅ | 响应式设计基准 |
| 视口尺寸 | window.innerHeight | 布局视口 高度 | ✅ | ⚠️ (极低) | ✅ | ✅ | 响应式设计基准 |
| 窗口尺寸 | window.outerWidth | 浏览器窗口整体宽度 | ✅ | ❌ | ✅ (含边框) | ❌ | 统计分析 / 窗口管理 |
| 窗口尺寸 | window.outerHeight | 浏览器窗口整体高度 | ✅ | ❌ | ✅ (含边框) | ❌ | 统计分析 / 窗口管理 |
| 窗口位置 | window.screenXwindow.screenLeft | 窗口距离屏幕左侧距离 | ✅ | ❌ | - | ❌ | 多屏窗口定位 |
| 窗口位置 | window.screenYwindow.screenTop | 窗口距离屏幕上侧距离 | ✅ | ❌ | - | ❌ | 多屏窗口定位 |
| 屏幕信息 | screen.width | 屏幕分辨率宽度 | ✅ | ❌ | - | ❌ | 设备指纹 / 适配 |
| 屏幕信息 | screen.availWidth | 屏幕可用宽度 (减去任务栏) | ✅ | ❌ | - | ❌ | 确定最大窗口尺寸 |
| 屏幕信息 | screen.availHeight | 屏幕可用高度 (减去任务栏) | ✅ | ❌ | - | ❌ | 确定最大窗口尺寸 |
| VisualViewport | visualViewport.width | 视觉视口 宽度 | ✅ | ❌ | ❌ | ✅ | 移动端缩放/键盘适配 |
| VisualViewport | visualViewport.height | 视觉视口 高度 | ✅ | ❌ | ❌ | ✅ | 移动端缩放/键盘适配 |
| VisualViewport | visualViewport.scale | 当前缩放比例 | ✅ | ❌ | - | ✅ | 检测用户是否缩放 |
| VisualViewport | visualViewport.offsetTop | 视觉视口相对于布局视口的偏移 | ✅ | ❌ | - | ✅ | 固定元素在缩放后定位 |