红宝书是学习和复习 JS 最好的书籍。
它适合有一定 JS 基础的人阅读,查缺补漏。不适合刚入门 JS 的人,因为太厚了,一字一句看需要太多精力,不太符合这个社会的快节奏。
一周看完这本书
前几天看完这本书,我就发了一个沸点。有人点赞,也有人回复说:不可能这么快看完。
我看书有一个习惯:对于重点的地方,会折页,也会拿比在书上写写画画。所以,从下图的折页来看,我应该是看完了。
分享一下我是如何一周看完这本书的(仅供参考,不能适用于所有人)
- 我日常业余时间就爱看闲书(不玩游戏),阅读习惯还可以,坐得住
- 春节前一周工作任务不重,每天可以看 5h 左右
- 我有一定的 JS 基础,熟悉的知识点不用一字一句看的很仔细(有些地方会反复看的很仔细)
- 某些我不常用的知识点我会略过,如 Canvas WebGL,以及 Web Worker 的一些细节
所以,我说的“看完”不是像看小说一样一字一句的看完。技术书籍是结构化、体系化的,而小说是线性的。
读书笔记
记录我个人感觉比较重点、或个人忽略的知识点。
浅层学习看输入,深度学习看输出。看完了要随便记录一下,写写画画才记的牢固。
Number 的范围
JS Number 是双精度 64 位浮点数。
最大值是 Number.MAX_VALUE (约 1.79E+308),最小值是 Number.MIN_VALUE(5e-324)。超出会用 Infinity 和 -Infinity 表示。
最大安全整数是 Number.MAX_SAFE_INTEGER(2^53 - 1),最小安全整数是 Number.MIN_SAFE_INTEGER(-(2^53 - 1)),超出计算会错误。例如:
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true ,不符合预期
Number 提供了一些 API 辅助判断数值范围,参考 MDN 文档。
Bigint 可表示任意大的整数,但仅仅是整数,非浮点数。
Number() 和 parseInt() parseFloat()
不推荐使用 Number() 转换数字,推荐使用 parseInt 和 parseFloat 。因为 Number() 转换的规则太多太琐碎,不容易记忆,而 parseInt 就简单很多。
Number(true) // 1
Number(false) // 0
parseInt(true) // NaN
parseInt(false) // NaN
Number(null) // 0
Number(undefined) // NaN
parseInt(null) // NaN
parseInt(undefined) // NaN
Number('') // 0
parseInt('') // NaN
但有一些 hack 操作会用到 Number() ,例如
+'1' // 1
+true // 1
2 - true // 1
所以,尽量不要用这些 hack 操作,写代码要简单、傻瓜式。或者直接转投 ts 。
\n 和 \r 的区别
\r 是回车,使光标回到行首,ACSII 码是 13 。\n 是换行,使光标下移一格,ACSII 码是 10 。这俩的定义源于之前的机械打印机,现代电脑键盘的 Enter 是这俩的组合。
前端开发中一般使用 \n 即可表示常规的换行。如果用 JS 替换换行符时,为了全面考虑,可以使用 /\n|\r\n/mg
Symbol.for 重用符号
Symbol('xxx') 返回一个不可重复的变量,但使用 Symbol.for 可以重复使用符号。
const a1 = Symbol.for('a')
const a2 = Symbol.for('a') // 重用符号
a1 === a2 // true
Symbol.keyFor(a1) // 'a'
const a3 = Symbol('a')
a1 === a3 // false
Symbol.keyFor(a3) // undefined
Object 获取所有 key (字符串 + Symbol)
如果 object key 有字符串,也有 symbol ,获取所有 key 要使用 Reflect.ownKeys(obj)
const obj = {
[Symbol('x')]: 100,
y: 200
}
Object.keys(obj) // ['y']
Object.getOwnPropertySymbols(obj) // [Symbol(x)]
Reflect.ownKeys(obj) // ['y', Symbol(x)]
for (const k in obj) {
console.log(k)
} // 'y'
可见,JS 中获取 Symbol key 并不方便,所以尽量不要用 Symbol 作为 key ,除非特殊用途。
for 循环中用 let 还是 const ?
原则是:尽量用 const,除非有可变量 (定义变量也是一样)
- 普通 for 循环,用 let
- for...in ,用 const
- for...of ,用 const
for (let i = 0; i < count; i++) { ... }
for (const k in obj) { ... }
for (const num of [1, 2, 3]) { ... }
标签语句
这是一个被忽略很久的语法,而且在 JS 中也不推荐使用。去年网上热议 Vue3 ref 语法糖,其中一个提案就是借用标签语句。
<script setup>
// 声明一个变量(这个变量将会被编译成一个ref)
ref: count = 1
function inc() {
// 该变量可以像普通变量那样使用
count++
}
// 想要获取到原本的变量的话需要在变量前面加一个💲符号
console.log($count.value)
</script>
<template>
<button @click="inc">{{ count }}</button>
</template>
其实 JS 标签语句的设计初衷是为了嵌套循环的,能配合 continue break 终止外层循环
let num = 0
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break outermost // 终止外层循环
}
num++
}
}
ES6 原生语法性能更优
为了浏览器兼容性,我们会使用 babel 把 ES6 编译成 ES5 。但随着现代浏览器的普及,大部分浏览器都已经原生支持 ES6 甚至更高版本。所以,如果你的产品能确定是现代浏览器使用,那可以直接使用 ES6 语法。性能更优。
例如,使用 let const 比 var 性能更好,因为前者是块级作用域,更利于垃圾回收。
再例如,使用 async-await 比转换为 Promise 性能更好,因为前者不用定义那么多函数,调用栈更少。
不要随意添加和删除 object 属性
推荐使用 class 定义模板,然后 new class 创建 object ,不要随意添加和删除 object 属性。
在 Chrome V8 中,多个相同属性的 object 会共享一个“隐藏类”,统一跟踪属性特征,更好的优化。如果属性一旦变化,那就需要重新创建一个“隐藏类”,额外增加开销。
如果需要增加、隐藏属性,那就用 Map ,别用 Object 。这两者是有明显的职责区分的。
数组长度是动态的
C 语言数组长度是固定的,而 JS 数组长度就是动态的。这是因为 JS 语法把它定义为动态,但其实内部依然是固定的。
JS 定义一个数组时,引擎会申请一个固定长度的空间。如果数组长度持续增加,超过了这个空间,引擎会生成一个更长的数组空间,把之前的数组内容拷贝过去。
所以,如果你能确定数组的长度比较长,如几百个,可以一次性定义一个足够长度的数组 const arr = new Array(500),有利于性能优化。
PS:数组的长度是有上限的,最多可以包含 4,294,967,295 ,日常使用足够了。(说不定没到上线,你的浏览器就崩溃了)
emoji 表情占两个字符
JS String 使用 16 位 Unicode 编码,每个字符都用 16 位表示,最多可以有 65,536 个字符。
const s = 'ab😁cd'
s.length // 6
但是 for...of 循环时,会把 emoji 当做一个完整的 item
let num = 0
for (const c of s) {
console.log(c)
num++
}
console.log(num) // 5
定型数组
如果纯 JS 编程,用不到定型数组,只有 JS 和其他方面交互时会用到。例如和 WebGL 原生库交换二进制数据,网络请求中需要二进制数据等。
定型数组的长度是固定的。
顺序和迭代
所有的有序结构,如 String Array 定型数组 Map Set 等(Object 不是),都默认有一个 Symbol.iterator 方法,执行返回一个迭代器对象。可以执行 next()方法,返回一个 { value, done }的结构。其实就是典型的迭代器模式。
const arr = [10, 20, 30]
const iteration = arr[Symbol.iterator]()
iteration.next() // {value: 10, done: false}
iteration.next() // {value: 20, done: false}
iteration.next() // {value: 30, done: false}
iteration.next() // {value: undefined, done: true}
其他 API 有可能返回一个迭代器对象,例如 Map Set 的 entries() keys() values() 。也可以通过传入一个可迭代对象来创建 Map Set 。
const m = new Map({
// 自定义迭代器
[Symbol.iterator]: function* () {
yield ['k1', 'v1']
yield ['k2', 'v2']
yield ['k3', 'v3']
}
})
所有实现 Symbol.iterator 方法的实例,都可以使用 for...of 进行遍历,都可以使用扩展操作符 ... 生成数组。
PS:Symbol.iterator 有时也会用 @@iterator 表示。
选择 Object 和 Map
Object 和 Map 的设计完全不同,编程中职责也不同。Object 是无序结构,属性有描述符,可用于 Proxy,而 Map 是有序结构,性能更好。所以 Object 适合于结构固定的实例和配置,而 Map 适合于内存存储,场景不同
两者性能对比
- 同样的数据,Map 内存使用更少(没有属性描述符,原型链简单)
- Map 的插入、删除的性能更好
循环不是迭代
普通的 for 循环遍历,需要得到以下信息
- 需要知道操作对象的
length和数据结构(如可通过[index]来获取 item) - 要求操作对象的数据顺序必须是
index累加的顺序,即最简单的有序结构,如数组
而现代编程中都需要能实现迭代器模式(上文已写),通过更灵活的方式进行迭代。默认可迭代的数据类型有
- String
- Array 定型数组
- Map
- Set
- arguments
- NodeList 等 DOM 集合
可迭代对象可用于
for...of- 数组解构
- 扩展操作符
Array.from()- 创建 Map
- 创建 Set
Promise.all()Promise.race()yield *(如下代码)
function* g1() {
for (const x of [1, 2, 3]) {
yield x
}
}
function* g2() {
yield* [1, 2, 3]
}
// g1 和 g2 效果相同
最简单的迭代器模式
了解了 JS 迭代器的实现规则,即可使用 Generator 函数 和 yield 创建一个最简单的迭代器模式。
class Foo {
constructor() {
this.values = [10, 20, 30]
}
// 自定义迭代器
* [Symbol.iterator]() {
yield* this.values
}
}
const f = new Foo()
for (const x of f) {
console.log(x)
}
使用 Generator 函数和 yield 递归 DOM 树
上文提到:循环不是迭代。有些比较复杂的有序结构,不能通过简单的 for 循环来遍历,迭代器模式更加灵活。
通过 Generator 函数和 yield 可以实现 DOM 树深度优先遍历,并生成一个可迭代对象。
function* traverse(nodes) {
for (const node of nodes) {
yield node
const children = node.children
if (children.length) {
yield* traverse(children)
}
}
}
const container = document.getElementById('container')
for (node of traverse([container])) {
console.log(node)
}
富文本编辑器 slate.js 有一个 API Node.nodes(root: Node, options?) 返回的就是一个迭代器。
对象属性描述符
可使用 Object.getOwnPropertyDescriptor(obj, 'key') 获取属性描述符,
Configurable属性是否可通过delete删除,是否可重新配置属性描述符,是否可定义getset,默认为trueEnumerable是否可以被for...in遍历到,默认为trueWritable值是否只读,默认为trueValue读取属性时返回的值
多年之前 for...in 遍历对象时,需要使用 hasOwnProperty 判断是否是原型属性,现在不用了。因为原型属性的 Enumerable 为 false,无法遍历到。
通过 Object.defineProperty() 可自定义属性描述符,但它会默认新属性的 Configurable Enumerable Writable 都为 false 。
Object.is() 和 ===
Object.is() 和 === 很像,但有些边界情况考虑的更加周到。如比较 0 +0 -0 ,比较 NaN
0 === +0 // true
0 === -1 // true
+0 === -0 // true
NaN === NaN // false
Object.is(0, +0) // true
Object.is(0, -0) // false
Object.is(-0, +0) // false
Object.is(NaN, NaN) // true
在普通使用场景中,用 === 就可以。用 Object.is() 反而会增加阅读成本,不一定所有人都了解这个 API 。
JS 实现抽象基类
Java 中的抽象类只能被继承,不能被实例化。JS 原生语法没有抽象类,抽象方法,但可以通过 new.target 判断。new.target 可获取当前 new 时执行的 class ,对比 class 即可。
class Foo() {
constructor() {
if (new.target === Foo) {
throw new Error('Abstract class cannot be directly instantiated.')
}
}
}
PS:TS 语法有抽象类
Proxy 不能修改 Object 属性特性
书中叫做“捕获器不变式”。Proxy 在 get set 源数据时,会受到对象属性描述符特性的“阻挡”。
const obj = { n: 10 }
Object.defineProperty(obj, 'x', {
value: 'x'
})
// 通过 Object.defineProperty 定义的属性,configurable enumerable writable 都默认为 false
const proxy = new Proxy(obj, {
get() {
return 'x1'
}
})
proxy.n // 'x1'
proxy.x // TypeError: 'get' on proxy: property 'x' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'x' but got 'x1')
Proxy 的坑
this- 代理之后,对象内部的this就有可能对应不起来instanceof- 代理之后,无法通过instanceof找到 class 。此时可直接代理 class ,不要代理实例
Proxy 应用场景
其实就是代理模式的应用场景。
- 跟踪属性 get 和 set ,例如 Vue3 实现响应式
- 隐藏属性,get 时判断属性 key ,返回
undefined - 属性验证,set 时判断 value ,不符合返回
false(即 set 失败)
书中还写了代理 class 时拦截 construct 来操作构造函数,我觉得这并不常用。
尾调用(尾递归)优化
普通的递归,代码执行时会持续积累调用栈,如果调用次数过多会出现 stack overflow 报错。 但如果符合尾调用的条件,JS 引擎就不会持续积累调用栈,而是重用栈帧。
尾调用优化有如下条件
- 严格模式下
- 外部函数的返回值,是对尾调用函数的调用(即直接 return 尾调用函数)
- 尾调用函数返回后,必须要再执行其他的逻辑
- 尾调用函数不是引用外部函数作用域中自由变量的闭包
下面是几个反例
"use strict"
// 例子1:无优化,因为没有 return 尾调用函数
function fn() {
innerFn()
}
// 例子2:无优化,因为尾调用函数没有**直接**返回
function fn() {
const res = innerFn()
return res
}
// 例子3:无优化,因为尾调用函数返回后还有其他逻辑(toString)
function fn() {
return innerFn().toString()
}
// 例子4:求斐波那契数列。无优化,尾调用函数返回后还有其他逻辑(相加)
function fib(n) {
if (n < 2) return n
return fib(n - 1) + fib(n - 2)
}
下面是几个正确的例子
"use strict"
// 有优化
function fn(a, b) {
return innerFn(a, b)
}
// 有优化
function fn(a, b) {
if (a < b) return a
return innerFn(a, b)
}
// 有优化
function fn(x) {
return x ? fn1() : fn2()
}
// 求斐波那契数列,尾调用优化
function fib(n) {
return _fibImp(0, 1, n)
}
function _fibImp(a, b, n) {
if (n === 0) return a
return _fibImp(b, a + b, n - 1)
}
作用域链和执行上下文
- 函数定义时,就会创建它的作用域链,预装在全局变量、父作用域变量,并保存在内部的
Scope中 - 函数执行时,创建执行上下文,定义内部变量、
this等,然后复制Scope生成完整的作用域链
所以,一个函数的上级作用域链是在它定义时生成的,而非执行时。函数内部的自由变量,也是在它定义时就确定的,而非执行时。
PS:闭包中的变量,默认是不会被垃圾回收的。除非引用闭包的函数被设置为 null ,这在编码中并不常见。
Promise 默认没有取消和进度跟踪
ES 标准认为 Promise 应该是激进的,即开始执行就不要再取消。否则会导致代码逻辑过于复杂,特别是在复杂场景和链式操作时。
代码应该首先保证逻辑清晰、易读,再说功能实现。这样才能保证较低的学习成本,和未来更高的可维护性。
有一些第三方 lib 可实现 Promise 取消和进度跟踪,但请慎重使用。
async/await 比 Promise 性能更好
使用 Promise 会定义很多函数,执行时会创建很多调用栈,而且都会保留在内存中。万一出错,还得抛出所有的调用栈信息。
而原生的 async/await 语法无需定义其他函数,执行时就不需要这么多调用栈。
PS:新的语法规范,除了更加简洁,性能也会更好。如 let const 比 var 语义更清晰,性能更好,上文已经提到过。
DPR
屏幕物理分辨率和逻辑分辨率的比例,通过 window.devicePixelRatio 可以获得。现在手机屏幕的 DPR 一般是 2-3 。
setInterval 的本质
setInterval 仅仅是定时把函数推到 Event Loop 的任务队列,它不关心函数何时执行、执行多久。
所以它无法保证函数一定会在间隔时间被执行,生产环境不要使用 setInterval 。如果持续执行定时任务,可以使用递归调用 setTimeout 的方式。
PS:还可以考虑使用 requestAnimationFrame 和 requestIdleCallback 。
URLSearchParams
解析和操作 query string 可以使用 URLSearchParams,简单方便
// 网络 url 是 'http://127.0.0.1:8881/?a=10&b=20'
const p = new URLSearchParams(location.search)
p.get('a') // '10'
p.get('b') // '20'
for (const item of p) { console.log(item) } // ['a', '10'] ['b', '20']
p.set('c', 30)
p.toString() // 'a=10&b=20&c=30'
借助前人成就,不要从 0 创新
2003 年,苹果宣布即将发布自己的浏览器 Safari 。Safari 的渲染引擎叫 WebKit ,是基于 Linux 平台浏览器 Konqueror 使用的渲染引擎 KHTML 开发的。
Chrome 一开始使用 Webkit 作为渲染引擎,后来基于 Webkit 开发了自己的渲染引擎 Blink 。
由此想到我开发 wangEditor V5 是借助 slate.js 为内核,而非自己从 0 开发内核,这一点还是非常明智的。slate.js 已然是开发维护了几年比较稳定、强大的产品了,我再做也不会它做的更好。
那些卖东西的人,在台上讲的都是各种创新的励志故事,而在台下就是集成、拼凑。苹果的核心技术也都不是自己研发的,也是收购各种科技公司,将它们的成果应用商业产品。
世界科技发展至今,都是一代一代人逐步积累过来的,越是复杂的事情,越不能从 0 创新。能学会前人技能并为自己所用,这本身就非常厉害。
cloneNode 只复制 attrs
cloneNode 只复制 HTML 属性,不会复制 JS 属性,也不会复制 JS 添加的 DOM 事件。
另,importNode 和 cloneNode 很相似,它是从其他 document 拷贝一个节点。
element.normalize()
- 清理空白 text Node
- 合并相邻的 text Node
MutationObserver 和 DOM 节点的引用
MutationObserver 拥有其目标节点的弱引用 (和 WeakMap WeakSet 一样)。目标节点却拥有 MutationObserver 的强引用 。如果目标节点在 DOM 中被删除,被垃圾回收,则 MutationObserver 也会被同时垃圾回收。
不要在内存中保留 MutationRecord ,因为它会包含对相关 DOM 节点的引用,会影响 DOM 节点的删除和垃圾回收。所以,计算 MutationRecord 获取想要的结果,单独记录下来,然后丢弃 MutationRecord 。
querySelectAll 不是“实时”查询 DOM
querySelectAll返回一个NodeList列表,但它仅仅是快照- 作为对比,
getElementsByTagName同样返回NodeList列表,但它是实时查询 DOM 变化的
如下代码,如果使用 getElementsByTagName 将会插入 11 个 <p> ,而使用 querySelectorAll 将会插入 2 个 <p>。
<div id="container">
<p>1</p>
</div>
const container = document.getElementById('container')
// const pList = document.getElementsByTagName('p')
const pList = document.querySelectorAll('p')
for (let i = 0; i < pList.length; i++) {
const newP = document.createElement('p')
newP.innerText = 'xxx'
container.appendChild(newP)
if (i >= 10) break
}
classList
分析和设置 DOM 节点的 CSS class 可以使用 elem.classList API,简单方便
addcontainsremovetoggletoStringfor...of遍历
焦点管理
document.hasFocus() 返回 boolean ,判断该网页是否有焦点
document.activeElement 指向焦点元素,如果无焦点则指向 <body>
focus 和 blur 事件不冒泡,最新的 focusin 和 blurout 事件可以冒泡。
document.readyState
loading网页加载中interactive可交互,相当于DOMContentLoadedcomplete完成,相当于onload
可通过 document.onreadystatechange 来监听 readyState 变化。
例如,我开发了一个第三方 SDK 要统一记录网页的性能,需要在 onload 时收集数据并发送出去。用户引用可能在网页 <head> 也可能是异步加载,不好控制时机。此时即可通过 readyState 值和变化来判断。
innerHTML 内存问题
用 innerHTML 可重新设置 HTML 内容并重新渲染 DOM 结构。但请注意:它不会删除被覆盖元素的 JS 属性和 DOM 事件,这可能会导致内存泄漏。
所以,正式项目中,尽量不要用这种简单粗暴的覆盖行为。
判断两个 DOM 节点的关系
containsAPI 可以判断两个节点的包含关系compareDocumentPositionAPI 可判断两个节点详细关系- 断开,节点不在文档中
- 领先
- 随后
- 包含
- 被包含
PS:DOM 节点 API 已经非常完善,不借助第三方 lib 即可很方便的操作
isSameNode 和 isEqualNode
isSameNode()同一个节点isEqualNode()属性相同的两个节点
style 和 getComputedStyle()
elem.style只是 HTML 标签上style属性的结构化,不会考虑其他 CSS 样式的影响getComputedStyle()获取的是全部 CSS 样式计算完成的结果,不仅仅是本身的style属性
深度优先遍历 DOM 节点 - NodeIterator 和 TreeWalker
NodeIterator 和 TreeWalker 是 JS 规范中自带的 API ,不用再自己手写遍历算法了。
PS:一般情况下,自带 API 都会比自己手写的性能好,特别是复杂场景下。
NodeIterator 可以通过 document.createNodeIterator() 创建。
const container = document.getElementById('container')
// 创建 iterator
const res = document.createNodeIterator(
container,
NodeFilter.SHOW_ELEMENT, // 还有 SHOW_ALL,SHOW_TEXT 等
{
acceptNode(node) {
return NodeFilter.FILTER_ACCEPT // 还有 FILTER_SKIP(跳过该节点)和 FILTER_REJECT(跳过改节点,和 childNodes)
}
},
false
)
// 遍历结果
let node = res.nextNode() // 还有 res.prevousNode()
while (node) {
console.log(node)
node = res.nextNode()
}
细心的会发现,以上的结果无法用于 for...of ,其实经过简单的改造即可
const iterator = {
res,
* [Symbol.iterator]() {
let node = this.res.nextNode()
while (node) {
yield node
node = res.nextNode()
}
}
}
for (const node of iterator) {
console.log(node)
}
TreeWalker 是 NodeIterator 的高级版本,它使用 document.createTreeWalker() 创建。和 createNodeIterator() 的参数一样。TreeWalker 的真正威力是它可以在 DOM 结构中“四处游走”:
walker.parentNode()walker.firstChild()walker.lastChild()walker.nextSibling()walker.previousSibling()
// 创建 walker
const walker = document.createTreeWalker(
container,
NodeFilter.SHOW_ELEMENT, // 还有 SHOW_ALL SHOW_TEXT 等
{
acceptNode(node) {
return NodeFilter.FILTER_ACCEPT // 还有 FILTER_SKIP FILTER_REJECT
}
},
false
)
// 进入 firstChild 再进入 nextSibling
walker.firstChild()
walker.nextSibling()
// 然后再开始遍历
let node = walker.nextNode()
while (node) {
console.log(node)
node = walker.nextNode()
}
两者对比
NodeIterator是一个双向链表TreeWalker是一棵树
stopImmediatePropagation
stopPropagation阻止事件冒泡stopImmediatePropagation阻止事件冒泡,并阻止任何后续事件处理程序
如下代码。如使用 stopPropagation 则只能阻止事件冒泡到 div1,而 p1 的三个事件都会被触发。如使用 stopImmediatePropagation 不仅能阻止事件冒泡到 div1,还能阻止后续 'p1 clicked - 3' 的触发。
即,同一节点、同一类型的事件,前面阻止了,后面就不会再继续触发了。
const container = document.getElementById('container')
const p1 = document.getElementById('p1')
container.addEventListener('click', e => {
console.log('container clicked')
})
p1.addEventListener('click', e => {
console.log('p1 clicked - 1')
})
p1.addEventListener('click', e => {
// e.stopImmediatePropagation()
e.stopPropagation()
console.log('p1 clicked - 2')
})
p1.addEventListener('click', e => {
console.log('p1 clicked - 3')
})
keypress 已废弃
keypress 已废弃,书中推荐使用 textInput 事件,但我没查到这个事件。感觉应该是 beforeinput 或者 input 事件。
键盘输入的触发顺序是 keydown beforeinput input keyup
const input1 = document.getElementById('input1')
input1.addEventListener('keydown', e => {
console.log('keydown', e.key)
})
input1.addEventListener('beforeinput', e => {
console.log('beforeinput', e.data)
})
input1.addEventListener('input', e => {
console.log('input', e.data)
})
input1.addEventListener('keyup', e => {
console.log('keyup', e.key)
})
判断网络状态
可以通过 navigator.onLine 判断是否联网,通过 navigator.connection 获取网络信息。
可以通过对 window 添加 online 和 offline 事件,随时监听网络变化。
判断网页是否显示
网页最小化,切换 tab ,就是不显示。
document.addEventListener('visibilitychange', () => {
console.log('visibilityState', document.visibilityState) // 'hidden' 或 'visible' 或 'prerender'
console.log('hidden', document.hidden)
})
performance.now()
Date.now()时间毫秒数的绝对值performance.now()返回一个相对值(从网页加载开始计时),带小数的毫秒时间间隔
performance.mark() 和 performance.measure() 也可用于高精确度的计算和分析性能。
不过就普通网页开发来说,使用 Date.now() 和 console.time() 这种毫秒精度的就可以满足要求。
网页性能数据
performance.timing 已经被废弃了,可以通过 performance.getEntriesByType('navigation')[0] 获取整个网页加载的性能数据。
通过 performance.getEntriesByType('resource') 可以获取各个静态资源(如各个图片)的加载性能。
通过 performance.getEntriesByType('paint') 可以获取 FP 和 FCP 的性能数据。
<template> 和 DocumentFragment
HTML 中的 <template> 不会渲染到页面上,内部会被当做 documentFragment 处理,也可以通过 JS 获取并操作。
<template id="tpl1">
<p>hello <b>world</b></p>
</template>
const tpl1 = document.getElementById('tpl1')
console.log(tpl1.content) // #document-fragment
console.log(tpl1.content instanceof DocumentFragment) // true
console.log(tpl1.content.children) // HTMLCollection
document.body.appendChild(tpl1.content) // 添加到网页
关于 ShadowDOM
可以内部设置 <style> 并且和 host 样式隔离。
但无法内部单独引入 <script> ,它要和 host 共享 JS 能力。但从 host 的 document 无法查询到 ShadowDOM 内部的节点。
ShadowDOM 有 <slot> 和自定义元素 is 属性,这和 Vue template 的设计一样。
安全随机数
Math.random() 返回一个随机数,使用简单,速度快。但它是伪随机数,并不是加密安全的随机数。
生产环境中生成安全随机数推荐使用 window.crypto.getRandomValues(),不过稍微麻烦一点点。
function genRandom() {
const arr = new Uint32Array(1) // 定型数组,要生成 32 位随机数
window.crypto.getRandomValues(arr) // 对于数组的每个元素,都会填充一个随机数
return arr[0] / 0xFFFFFFFF // 生成 0-1 小数形式,和 Math.random() 保持一致 (0xFFFFFFFF 是 32 位数字的最大值)
}
PS:window.crypto 还可以进行很多加密操作,这方面我并不精通。
Beacon API
如果想要在网页 unload 之前安全的发出信息,使用 fetch XHR 都不合适,有专门的 API 做这件事。
window.onunload = () => {
navigator.sendBeacon(url, 'xxx')
}
Web Worker 本质是线程
书中翻译为“工作者线程”。Web Workder 本质也是浏览器通过 Thread 实现的。
某些 Worker 甚至都和网页不属于同一个进程,浏览器本身就是多进程架构的,SharedWorker 和 ServiceWorker 都有自己独立的进程。
但这并不影响 JS 是单线程执型的特点。
总结
很多年之前我看过红宝书第三版,现在再看第四版,感觉这几年 JS 相关的变化非常大,学习成本是越来越高了。
而且,API 越多,功能越复杂,就越需要一款稳定的语言,所以推荐尽快拥抱 TS 。
—— 2022 正月初四