红宝书《Javascript 高级程序设计-第4版》读书笔记

4,331 阅读9分钟

红宝书是学习和复习 JS 最好的书籍。
它适合有一定 JS 基础的人阅读,查缺补漏。不适合刚入门 JS 的人,因为太厚了,一字一句看需要太多精力,不太符合这个社会的快节奏。

一周看完这本书

前几天看完这本书,我就发了一个沸点。有人点赞,也有人回复说:不可能这么快看完。

我看书有一个习惯:对于重点的地方,会折页,也会拿比在书上写写画画。所以,从下图的折页来看,我应该是看完了。

image.png

分享一下我是如何一周看完这本书的(仅供参考,不能适用于所有人)

  • 我日常业余时间就爱看闲书(不玩游戏),阅读习惯还可以,坐得住
  • 春节前一周工作任务不重,每天可以看 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_INTEGER2^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() 转换数字,推荐使用 parseIntparseFloat 。因为 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 删除,是否可重新配置属性描述符,是否可定义 get set,默认为 true
  • Enumerable 是否可以被 for...in 遍历到,默认为 true
  • Writable 值是否只读,默认为 true
  • Value 读取属性时返回的值

多年之前 for...in 遍历对象时,需要使用 hasOwnProperty 判断是否是原型属性,现在不用了。因为原型属性的 Enumerablefalse,无法遍历到。

通过 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 生成完整的作用域链

image.png

所以,一个函数的上级作用域链是在它定义时生成的,而非执行时。函数内部的自由变量,也是在它定义时就确定的,而非执行时。

PS:闭包中的变量,默认是不会被垃圾回收的。除非引用闭包的函数被设置为 null ,这在编码中并不常见。

Promise 默认没有取消进度跟踪

ES 标准认为 Promise 应该是激进的,即开始执行就不要再取消。否则会导致代码逻辑过于复杂,特别是在复杂场景和链式操作时。

代码应该首先保证逻辑清晰、易读,再说功能实现。这样才能保证较低的学习成本,和未来更高的可维护性。

有一些第三方 lib 可实现 Promise 取消和进度跟踪,但请慎重使用。

async/await 比 Promise 性能更好

使用 Promise 会定义很多函数,执行时会创建很多调用栈,而且都会保留在内存中。万一出错,还得抛出所有的调用栈信息。

而原生的 async/await 语法无需定义其他函数,执行时就不需要这么多调用栈。

PS:新的语法规范,除了更加简洁,性能也会更好。如 let constvar 语义更清晰,性能更好,上文已经提到过。

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,简单方便

  • add
  • contains
  • remove
  • toggle
  • toString
  • for...of 遍历

焦点管理

document.hasFocus() 返回 boolean ,判断该网页是否有焦点
document.activeElement 指向焦点元素,如果无焦点则指向 <body>

focusblur 事件不冒泡,最新的 focusinblurout 事件可以冒泡。

document.readyState

  • loading 网页加载中
  • interactive 可交互,相当于 DOMContentLoaded
  • complete 完成,相当于 onload

可通过 document.onreadystatechange 来监听 readyState 变化。

例如,我开发了一个第三方 SDK 要统一记录网页的性能,需要在 onload 时收集数据并发送出去。用户引用可能在网页 <head> 也可能是异步加载,不好控制时机。此时即可通过 readyState 值和变化来判断。

innerHTML 内存问题

用 innerHTML 可重新设置 HTML 内容并重新渲染 DOM 结构。但请注意:它不会删除被覆盖元素的 JS 属性和 DOM 事件,这可能会导致内存泄漏。

所以,正式项目中,尽量不要用这种简单粗暴的覆盖行为。

判断两个 DOM 节点的关系

  • contains API 可以判断两个节点的包含关系
  • compareDocumentPosition API 可判断两个节点详细关系
    • 断开,节点不在文档中
    • 领先
    • 随后
    • 包含
    • 被包含

PS:DOM 节点 API 已经非常完善,不借助第三方 lib 即可很方便的操作

isSameNode 和 isEqualNode

  • isSameNode() 同一个节点
  • isEqualNode() 属性相同的两个节点

style 和 getComputedStyle()

  • elem.style 只是 HTML 标签上 style 属性的结构化,不会考虑其他 CSS 样式的影响
  • getComputedStyle() 获取的是全部 CSS 样式计算完成的结果,不仅仅是本身的 style 属性

深度优先遍历 DOM 节点 - NodeIteratorTreeWalker

NodeIteratorTreeWalker 是 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)
}

TreeWalkerNodeIterator 的高级版本,它使用 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 添加 onlineoffline 事件,随时监听网络变化。

判断网页是否显示

网页最小化,切换 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 正月初四