《JavaScript 高级程序设计》第2-5章

2024年03月11日

2. 变量、作用域与内存

原始值与引用值

  • 变量包含两种不同类型的数据:原始值引用值
  • 原始值和引用值在通过变量复制时,都会被复制到新变量所在的位置,区别在于引用值复制过去的实际上是一个指向存储在堆内存中对象的指针。
  • 所有函数的参数都是按值传递
  • **typeof **操作符用于判断一个变量是否为原始类型,instanceof 操作符用于判断变量是否为给定的引用类型的实例

执行上下文与作用域

  • 每个上下文都有一个关联的变量对象(无法通过代码访问),这个上下文中定义的所有变量和函数都存在于这个对象上。而上下文代码在执行时,会创建变量对象的一个作用域链。代码正在执行的上下文的变量对象始终位于作用域链的最前端(如果上下文是函数,则其活动对象用作变量对象),作用域链中的下一个变量对象来自包含上下文,在下一个对象来自下一个包含上下文。以此类推直至全局上下文。全局上下文的变量对象始终是作用域链的最后一个变量对象。
  • 函数参数被认为是当前上下文中的变量,跟其他变量遵循相同的访问规则
  • try/catch 语句的 catch 块和 with 语句会导致在作用域链前端添加一个上下文,这个上下文在代码执行后会被删除

垃圾回收

  • JavaScript 最常用的垃圾回收策略是标记清理,不常用的回收策略是引用计数
  • 优化内存占用的最佳手段是保证在执行代码时只保存必要的数据,数据不再必要就设置为 null 从而释放引用,叫做解除引用
    • 通过 const 和 let 声明提升性能
    • V8引擎中会将解释后的 JavaScript 代码编译为实际机器码时会利用隐藏类,为了提升性能让实例共享隐藏类,应当避免 **“先创建再补充”**的动态属性赋值,并在构造函数中一次性声明所有属性
    • 使用 delete 关键字时也会导致生成相同的隐藏类片段,最佳实践为将不想要的属性设置为 null
  • JavaScript 中的内存泄漏大部分是由不合理的引用导致的:
    • 意外声明全局变量
    • 定时器
    • 闭包
  • 浏览器何时运行垃圾回收程序的一个标准是对象更替的速度,可考虑通过对象池静态分配的方式进一步提升性能

3. 基本引用类型

引用值(对象)是某个特定引用类型的实例。引用类型有时也被称为对象定义,它们描述了自己的对象应有的属性和方法。

Date

  • Date.parse() 接收表示日期的字符串返回表示该日期的毫秒数
  • Date.UTC() 接受逗号分隔的年、月、日、时、分、秒、毫秒并返回该日期的毫秒数
  • Date.now() 返回方法执行时时期和时间的毫秒数
  • toLocaleString() 和 toString() 返回日期和时间
  • valueOf() 返回日期的毫秒表示
  • 注意:getMonth() 方法返回的月份从 0 开始(0-11)

RegExp

  • ECMAScript 通过 RegExp 类型支持正则表达式
  • exec() 方法用于配合捕获组,返回包含第一个匹配信息的数组,如果未找到匹配项则返回 null
  • test() 方法接收一个字符串参数,如果输入文本和模式匹配返回 true,否则返回 false

原始值包装类型

  • 特殊的引用类型:Boolean、Number 和 String
  • 当使用某个原始值的方法或属性,后台会创建一个相应包装类型的对象从而暴露操作原始值的方法,随后摧毁创建的实例

Number 类型

  • toFixed() 方法返回包含小数点位数的数值字符串
  • toExponential() 返回科学计数法表示的数值字符串
  • toPrecision() 根据情况返回合理的输出结果
  • ES6 新增 Number.isInteger() 判断一个数值是否为整数

String 类型

  • charAt() 返回给定索引位置的字符
  • concat() 将一个或多个字符串拼接成新字符串
  • 从字符串提取子字符串:
    • slice(startIndex, endIndex) 如果参数为负值,取字符串长度加上负参数值
    • substr(startIndex, length) 如果第一个参数为负值,取字符串长度加上负值;第二个参数为负值则转换为0
    • substing(startIndex, endIndex) 所有负参数值都转换为0
  • 字符串中定位子字符串:indexOf() 和 lastIndexOf(),这两个方法都可以接收第二个参数表示开始搜索的位置(从头和从末尾搜索)
  • 判断字符串中是否包含另一个字符串:
    • startsWith() 检索索引0的匹配项,返回一个布尔值;可接受第二个参数表示开始搜索的位置
    • endsWith() 检索最后一个匹配项,返回一个布尔值;可接受第二个参数表示当作字符串结尾的位置
    • includes() 检索整个字符串,可接受第二个参数表示开始搜索的位置
  • trim() 创建字符串的一个副本并删除前后所有空格,也可以使用 trimLeft() 和 trimRight() 单独删除开头或末尾的空格
  • repeat() 接收整数参数,表示将字符串复制多少次并拼接返回
  • padStart() 和 padEnd() 复制字符串,如果小于指定长度则以空格或填充字符串填充剩余位置,第一个参数为长度,第二个参数为可选填充字符串默认为空格
  • @@iterator 表示可迭代字符串的每个字符
javascript 复制代码
// 手动使用迭代器
let message = 'abc'
let stringIterator = message[Symbol.iterator]()
console.log(stringIterator.next()) // {value:'a', done:false}
console.log(stringIterator.next()) // {value:'b', done:false}
console.log(stringIterator.next()) // {value:'c', done:false}
console.log(stringIterator.next()) // {value:undefined, done:true}

// 使用 for-of 循环访问
for (const c of 'abcde') {
  console.log(c)
}

// 用解构操作解构
let message = 'abcde'
console.log([...message]) // ['a', 'b', 'c', 'd', 'e']
  • toLowerCase()、toLocaleLowerCase()、toUpperCase()、toLocaleUpperCase() 大小写转换
  • 字符串模式匹配方法:
    • match() 返回数组,第一个元素是与整个模式匹配的字符串,其余元素是与表达式中的捕获组匹配的字符串
    • search() 返回模式第一个匹配的位置索引,没有找到则返回 -1
    • replace() 第一个参数为字符串或 RegExp 对象,第二个参数为要替换成的字符串或函数。如果第二个参数为函数,则会收到3个参数:与模式匹配的字符串,匹配项在字符串中的开始位置,整个字符串,根据函数的返回值替换
  • split() 根据传入的分隔符将字符串拆分为数组,还可以传入第二个参数(数组大小)用于控制该函数返回的数组长度不会超过指定大小

单例内置对象

Global

  • Global 对象是 ECMAScript 中最特别的对象,代码不会显式的访问它
  • encodeURI() 和 encodeURIComponent() 编码统一资源标识符(URI)
  • decodeURI() 和 decodeURIComponent() 解码
  • eval() 是一个完整的 ECMAScript 解释器,接收一个参数,即一个要执行的 ECMAScript 字符串
  • window 对象是浏览器实现 Global 对象的代理

Math

  • Math 对象上提供的计算要比直接使用 JavaScript 快得多
  • min() 和 max() 方法确定一组数值中的最小值和最大值
  • 舍入方法:
    • Math.ceil() 向上舍入
    • Math.floor() 向下摄入
    • Math.roud() 四舍五入
    • Math.fround() 返回数值最接近的单精度(32位)浮点值表示
  • random() 返回一个 0~1 范围内的随机数,包括 0 但不包括 1。为了加密使用生成的随机数建议使用 window.crypto.getRandomValues()

4. 集合引用类型

Object

Object 是 ECMAScript 中最常用的类型之一,很适合存储和在应用程序之间交换数据。
构建方式:

  1. new 操作符和 Object 构造函数
  2. 对象字面量表示法

可以通过点语法或中括号来存取属性。

Array

  • 构建方式:
    1. Array 构造函数
    2. 数组字面量表示法
    3. Array.from() 将类数组结构转换为数组实例,可接收第二个可选的映射函数参数用于增强新数组的值
javascript 复制代码
const a1 = [1, 2, 3, 4]
const a2 = Array.from(a1, x => x**2)
console.log(a2) // [1, 4, 9, 16]
复制代码
  4. Array.of() 将一组参数转换为数组实例
  • 数组字面量初始化数组时,可以使用一串逗号来创建空位。在ES6新增方法普遍将这些空位设置为 undefined
  • 取得或设置数组的值使用中括号并提供对应值的数字索引
  • Array.isArray() 用于确定一个值是否为数组
  • 迭代器方法:
    • keys() 返回数组索引的迭代器
    • values() 返回数组元素的迭代器
    • entries() 返回索引/值对的迭代器
  • 复制和填充方法:
    • fill() 向已有数组中插入全部或部分相同的值,参数为填充的值,开始索引(可选),结束索引(可选)
    • copyWithin() 按照指定范围浅复制数组部分内容,插入指定索引开始的位置,参数为插入索引,复制索引开始(可选),复制索引结束(可选);超出数组边界、零长度及方向相反的索引范围会被忽略
  • join() 返回指定字符串分隔符分隔的数组所有元素组成的字符串,不传参数默认使用逗号分隔
  • 数组提供了 push() 和 pop() 方法实现类似栈的行为
  • 使用 shift() 和 push() 方法实现队列行为
  • 使用 unshift() 和 pop() 方法实现反向队列
  • 排序方法:
    • reverse() 反向排列数组
    • sort() 默认将每一项数组元素上调用 String() 转型函数再按照升序排序。可以接收一个比较函数用于判断哪个值排在前面
javascript 复制代码
function compare(value1, value2) {
  return value2 - value1
}
  • reverse() 和 sort() 方法都返回调用它们的数组的引用
  • 操作方法:
    • concat() 在现有数组基础上拼接元素创建一个新数组
    • slice() 接收两个参数:返回元素的开始索引和结束索引(可选),返回两个索引之间,不包括结束索引位置的所有元素。该方法不会影响原始数组
    • splice() 在数组之间插入元素,修改原始数组
      • 删除 参数:要删除的第一个元素位置和要删除的元素数量
      • 插入 参数:开始位置、0(要删除的元素数量)、在数组指定位置插入的元素(可以插入多个元素)
      • 替换 参数:开始位置、要删除的元素数量、在数组指定位置插入的元素(可以插入多个元素)
  • 搜索和位置方法
    • 严格相等:indexOf()、lastIndexOf()、includes()
    • 断言函数:find()、findIndex()
  • 迭代方法
    • every() 每一项都返回 true 则 返回 true
    • filter() 返回 true 的每一项组成数组返回
    • forEach() 没有返回值
    • map() 返回每次函数调用结果构成的数组
    • some() 一项返回 true 则返回 true
  • 归并方法:reduce() 和 reduceRight(),这两个方法都接收两个参数,对每一项都会运行的归并函数以及可选的以之为起点的初始值。归并函数接收4个参数:上一个归并值、当前项、当前项的索引和数组本身。例如使用 reduce 执行累加数组中所有数值:
javascript 复制代码
let value = [1, 2, 3, 4, 5]
let sum = value.reduce((prev, cur, index, array) => prev + cur)
console.log(sum) // 15

Map

键/值存储的集合类型,可通过 set 方法添加、get 和 has 查询,通过 size 属性获取键值对数量,使用 delete 和 clear 删除值。

  • 与 Object 类型不同, Map 实例会维护键值对插入顺序,使用 entries 方法或 Symbol.iterator 属性获取迭代器进行迭代,也可使用 forEach 方法传入回调
  • 使用 Object 还是 Map
    • 固定内存大小 Map 可以存储更多的键值对
    • Map 的插入性能更佳
    • Object 的查找性能更佳
    • Map 的删除性能更佳

WeakMap

  • 弱映射中的键只能是 Object 或继承自 Object 的类型
  • 使用:
    • 实现私有变量
    • 保存DOM节点元数据

Set

集合数据结构,使用 new 关键字和 Set 构造函数创建,可通过给构造函数传入一个可迭代对象创建时初始化。使用 add 方法增加值,has 方法查询,size 获取元素数量,delete 和 clear 删除元素。

  • 使用 values 及其别名方法 keys 或 Symbol.iterator 属性获取迭代器,也可使用 forEach 方法传入回调

迭代和扩展操作

  • Array、定型数组、Map 和 Set 原生集合类型都定义了默认迭代器,因此均可使用 for-of 循环
  • 也可以兼容扩展操作符。扩展操作符在对可迭代对象执行浅复制时十分有用
javascript 复制代码
let arr1 = [1, 2, 3]
let arr2 = [...arr1]

5.迭代器与生成器

理解迭代

  • 循环是迭代机制的基础,因为它可以指定迭代的次数,以及每次迭代要执行什么操作。
  • 迭代会在一个有序集合上进行(“有序”可以理解为集合中所有项都可以按照既定的顺序被遍历到,特别是开始和结束项有明确的定义)
  • 通过循环进行迭代:
    • 必须事先知道如何使用数据结构
    • 便利顺序不是数据结构固有的
  • 为解决上述问题,实现开发者无须事先知道如何迭代就能进行迭代操作,出现了迭代器模式

迭代器模式

迭代器模式是指可以把某些结构称为“可迭代对象”,它们实现了正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费。
迭代器是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露其可迭代对象的API。

可迭代协议

实现 Iterable 接口要求同时具备两种能力:

  1. 支持迭代的自我识别能力
  2. 创建实现 Iterator 接口的对象的能力

在 ECMAScript中,通过以 Symbol.iterator 作为键的属性,引用一个迭代器工厂函数。调用这个工厂函数必须返回一个新迭代器。
实现 Iterator 接口的内置属性包括:

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments 对象
  • NodeList 等DOM集合类型

接收可迭代对象的原生语言特性包括:

  • for-of 循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all() 接收由 Promise 组成的可迭代对象
  • Promise.race() 接收由 Promise 组成的可迭代对象
  • yield* 操作符

上述原生语言结构会在后台调用提供的可迭代对象的这个工厂函数创建一个迭代器。
如果对象原型链上的父类实现了 Iterable 接口,那这个对象也就实现了这个接口。

迭代器协议

实现 Iterator 接口要求提供一个 next 方法,每次成功调用都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值,
IteratorResult 对象包含 value 和 done 两个属性。done 为布尔值,表示是否还可以调用 next 方法获取下一个值;value 为包含可迭代对象的下一个值或 undefined (done 为 true)。
需要注意的是:

  • 每个迭代器之间没有联系,独立遍历可迭代对象
  • 迭代器只是通过游标来记录遍历可迭代对象,当对象在迭代期间改变,迭代器也会反映相应的变化
  • 迭代器维护一个指向可迭代对象的引用,会阻止垃圾回收

自定义迭代器

javascript 复制代码
class Counter {
    constructor(limit) {
        this.limit = limit;
    }

    [Symbol.iterator]() {
        let count = 1
        let limit = this.limit
        return {
            next() {
                return count <= limit ?
                    {done: false, value: count++} :
                    {done: true, value: undefined}
            }
        }
    }
}

let counter = new Counter(3)
for (let i of counter) {
    console.log(i)
}
// 1
// 2
// 3
for (let i of counter) {
    console.log(i)
}
// 1
// 2
// 3

提前终止迭代器

可选的 return 方法用于指定在迭代器提前关闭时执行的逻辑。迭代器提前关闭可能的情况包括:

  • for-of 循环通过 break、continue、return 或 throw 提前退出
  • 解构操作并未消费所有值

如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。数组的迭代器是不能关闭的。
可以测试迭代器实例的 return 属性是不是函数对象来确定这个迭代器能否关闭。不能关闭的迭代器增加 return 方法并不能让其变为可关闭的。

生成器

生成器拥有在一个函数块内暂停和恢复代码执行的能力。

生成器基础

生成器的形式是一个函数,函数名称前加一个星号(*)表示它是一个生成器。

javascript 复制代码
function* generatorFn() {}

箭头函数不能用于定义生成器函数。
调用生成器函数会产生生成器对象,默认处于暂停执行状态。生成器对象也实现了 Iterator 接口,因此具有 next() 方法。调用 next 方法会让生成器开始或恢复执行。

  • next 方法返回值类似迭代器,有 done 和 value 属性
  • 生成器函数只有在初次调用 next 方法后开始执行

通过 yield 中断执行

yield 关键字可以让生成器停止和开始执行。生成器函数在遇到 yield 之前会正常执行,遇到关键字后执行停止并保留函数作用域状态,只有在生成器对象上调用 next 方法后才能恢复执行。

  • yield 关键字退出的生成器函数处在 done:false 状态,return 退出处于 done:true 状态
  • 生成器对象之间也不会互相影响
  • 生成器对象可作为可迭代对象
javascript 复制代码
function* generatorFn() {
  yield 1
  yield 2
  yield 3
}

for (const x of generatorFn()) {
  console.log(x)
}
// 1
// 2
// 3
  • yield 可实现输入和输出
javascript 复制代码
function* generatorFn() {
  return yield 'foo'
}
let g = generatorFn()
console.log(g.next()) // {done: false, value: 'foo'}
console.log(g.next('bar')) // {done: true, value: 'var'}
  • yield* 可用于产生可迭代对象,将可迭代对象序列化为一连串可以单独产出的值
javascript 复制代码
function* generatorFn() {
  yield* [1, 2, 3]
}
for (const x of generatorFn()) {
  console.log(x)
}
// 1
// 2
// 3
  • yield* 可用于实现递归算法
javascript 复制代码
function* nTimes(n) {
  if (n > 0) {
    yield* nTimes(n - 1)
    yield n - 1
  }
}
for (const x of nTimes(3)) {
  console.log(x)
}
// 0
// 1
// 2

生成器作为默认迭代器

javascript 复制代码
class Foo {
    constructor() {
        this.values = [1, 2, 3]
    }

    * [Symbol.iterator]() {
        yield* this.values
    }
}

const f = new Foo()
for (const x of f) {
    console.log(x)
}
// 1
// 2
// 3

提前终止生成器

return 方法会强制生成器进入停止状态。提供给 return 方法的值就是终止迭代器对象的值。
所有的生成器对象都有 return 方法,并且关闭状态后无法恢复。for-of 等内置解构会忽略关闭后生成器对象内部返回的值。
throw 方法会在暂停的时候将一个提供的错误注入到生成器对象中,如果生成器函数内部没有处理这个错误,那么生成器就会关闭;如果处理了则会跳过当前 yield 恢复执行。
如果生成器对象还没有开始执行,那么调用 throw 方法抛出的错误不会在函数内部被捕获

相关文章

Vite项目配置本地HTTPS

React Native 开发环境安装踩坑

《JavaScript 高级程序设计》第10-16章