# 定型数组

定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率。JavaScript 并没有 TypedArray 类型,他所指的其实是一种特殊的包含数值类型的数组。

定型数组也是当时为了解决 webgl 与 JavaScript 运行时之间传递数据而生成的,因为 webgl 数据精度与 JavaScript 的精度不一致。JavaScript 就提供了一个接口 Float32Array,这个数组可以直接传给底层图形驱动程序 API,也可以直接从底层获取到。

# ArrayBuffer

Float32Array 实际上是一种“视图”,可以允许 JavaScript 运行时访问一块名为 ArrayBuffer 的预分配内存。

  • ArrayBuffer 是所有定型数组及视图引用的基本单位。SharedArrayBuffer 是 ArrayBuffer 的一个变体,可以无须复制就在执行上下文间传递它。

  • ArrayBuffer() 是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间。

const buf = new ArrayBuffer(16) // 在内存中分配 16 个字节
console.log(buf.byteLength) // 16
  • ArrayBuffer 一经创建就不能再调整大小。不过,可以使用 slice() 复制其全部或部分到一个新实例中;
const buf1 = new ArrayBuffer(16)
const buf2 = buf1.slice(4, 12)
console.log(buf2.byteLength) // 8
  • ArrayBuffer 在分配失败时会抛出错误。
  • ArrayBuffer 分配的内存不能超过 Number.MAX_SAFE_INTEGER (2^53 - 1)字节。
  • 声明 ArrayBuffer 则会将所有二进制位初始化为 0。
  • 声明 ArrayBuffer 分配的堆内存可以被当成垃圾回收,不用手动释放。

不能仅通过对 ArrayBuffer 的引用就读取或写入其内容。要读取或写入 ArrayBuffer ,就必须通过视图。视图有不同的类型,但引用的都是 ArrayBuffer 中存储的二进制数据。

# DataView

DataView 可以读写 ArrayBuffer 的视图,这个视图专为文件 I/O 和网络 I/O 设计,其 API 支持对缓冲数据的高度控制,但相比于其他类型的视图性能也差一些。 DataView 对缓冲内容没有任何预设,也不能迭代。

必须在对已有的 ArrayBuffer 读取或写入时才能创建 DataView 实例。这个实例可以使用全部或部分 ArrayBuffer,且维护着对该缓冲实例的引用,以及视图在缓冲中开始的位置。

const buf = new ArrayBuffer(16)

// DataView 默认使用整个 ArrayBuffer
const fullDataView = new DataView(buf)
console.log(fullDataView.byteOffset) // 0
console.log(fullDataView.byteLength) // 16
console.log(fullDataView.buffer === buf) // true

// 构造函数接收一个可选的字节偏移量个字节长度
// byteOffset = 0  表示视图从缓冲起点开始
// byteLength = 8 表示视图为前 8 个字节
const firstHalfDataView = new DataView(buf, 0, 8);
console.log(fullDataView.byteOffset) // 0
console.log(fullDataView.byteLength) // 8
console.log(fullDataView.buffer === buf) // true

// 如果不指定,则 DataView 会使用剩余的缓冲
// byteOffset = 8 表示视图从缓冲的第 9 个字节开始
// byteLength 未指定,默认为剩余缓冲
const secondHalfDataView = new DataView(buf, 8)
console.log(fullDataView.byteOffset) // 8
console.log(fullDataView.byteLength) // 8
console.log(fullDataView.buffer === buf) // true

要通过 DataView 读取缓冲,还需要几个组件。

  • 首先是要读或写的字节偏移量。可以看成 DataView 中的某种“地址”。
  • DataView 应该使用 ElementType 来实现 JavaScript 的 Number 类型到缓冲内二进制格式的转换。
  • 最后是内存中值的字节序。默认为大端字节序。
  1. ElementType

DataView 对存储在缓冲内的数据类型没有预设。它暴露的 API 强制开发者在读、写时指定一个ElementType,然后 DataView 就会忠实地为读、写而完成相应的转换。 ECMAScript 6 支持 8 种不同的 ElementType:

avatar

DataView 为上表中的每种类型都暴露了 get 和 set 方法,这些方法使用 byteOffset (字节偏移量)定位要读取或写入值的位置。类型是可以互换使用的,如下例所示:

// 在内存中分配两个字节并声明一个 DataView
const buf = new ArrayBuffer(2)
const view = new DataView(buf)

// 说明整个缓冲确实所有二进制位都是 0
// 检查第一个和第二个字符
console.log(view.getIn8(0)) // 0
console.log(view.getIn8(1)) // 0
// 检查整个缓冲
console.log(view.getInt16(0)) // 0

// 将整个缓冲都设置为 1
// 255 的二进制表示是 11111111(2^8 - 1)
view.setUint8(0, 255)

// DataView 会自动将数据转换为特定的 ElementType
// 255 的十六进制表示是 0xFF
view.setUint8(1, 0xFF)
// 现在,缓冲里都是 1 了
// 如果把它当成二补数的有符号整数,则应该是 -1
console.log(view.getInt16(0)) // -1
  1. 字节序 “字节序”指的是计算系统维护的一种字节顺序的约定。DataView 只支持两种约定:大端字节序和小端字节序。大端字节序也称为“网络字节序”,意思是最高有效位保存在第一个字节,而最低有效位保存在最后一个字节。小端字节序正好相反,即最低有效位保存在第一个字节,最高有效位保存在最后一个字节。

JavaScript 运行时所在系统的原生字节序决定了如何读取或写入字节,但 DataView 并不遵守这个约定。对一段内存而言, DataView 是一个中立接口,它会遵循你指定的字节序。 DataView 的所有 API 方法都以大端字节序作为默认值,但接收一个可选的布尔值参数,设置为 true 即可启用小端字节序。

// 在内存中分配两个字节并声明一个 DataView
const buf = new ArrayBuffer(2)
const view = new DataView(buf)
// 填充缓冲,让第一位和最后一位都是 1
view.setUint8(0, 0x80) // 设置最左边的位等于 1
view.setUint8(1, 0x01) // 设置最右边的位等于 1
// 缓冲内容(为方便阅读,人为加了空格)
// 0x8 0x0 0x0 0x1
// 1000 0000 0000 0001
// 按大端字节序读取 Uint16
// 0x80 是高字节,0x01 是低字节
// 0x8001 = 2^15 + 2^0 = 32768 + 1 = 32769
console.log(view.getUint16(0)) // 32769
// 按小端字节序读取 Uint16
// 0x01 是高字节,0x80 是低字节
// 0x0180 = 2^8 + 2^7 = 256 + 128 = 384
console.log(view.getUint16(0, true)) // 384
// 按大端字节序写入 Uint16
view.setUint16(0, 0x0004)
// 缓冲内容(为方便阅读,人为加了空格)
// 0x0 0x0 0x0 0x4
// 0000 0000 0000 0100
console.log(view.getUint8(0)) // 0
console.log(view.getUint8(1)) // 4
// 按小端字节序写入 Uint16
view.setUint16(0, 0x0002, true)
// 缓冲内容(为方便阅读,人为加了空格)
// 0x0 0x2 0x0 0x0
// 0000 0010 0000 0000
console.log(view.getUint8(0)) // 2
console.log(view.getUint8(1)) // 0
  1. 边界情形

DataView 完成读、写操作的前提是必须有充足的缓冲区,否则就会抛出 RangeError :

const buf = new ArrayBuffer(6)
const view = new DataView(buf)
// 尝试读取部分超出缓冲范围的值
view.getInt32(4)
// RangeError
// 尝试读取超出缓冲范围的值
view.getInt32(8)
// RangeError
// 尝试读取超出缓冲范围的值
view.getInt32(-1)
// RangeError
// 尝试写入超出缓冲范围的值
view.setInt32(4, 123)
// RangeError

DataView 在写入缓冲里会尽最大努力把一个值转换为适当的类型,后备为 0。如果无法转换,则抛出错误:

const buf = new ArrayBuffer(1)
const view = new DataView(buf)
view.setInt8(0, 1.5);
console.log(view.getInt8(0)) // 1
view.setInt8(0, [4])
console.log(view.getInt8(0)) // 4
view.setInt8(0, 'f')
console.log(view.getInt8(0)) // 0
view.setInt8(0, Symbol())
// TypeError

# Map

Map 是 ECMAScript 6 的新增特性,一种新的集合类型;

# 基本 API

使用 new 关键字和 Map 构造函数可以创建一个空映射:

const m = new Map()

如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含键/值对数组。可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中:

// 使用嵌套数组初始化映射
const m1 = new Map([
  ['key1', 'val1'],
  ['key2', 'val2'],
  ['key3', 'val3'],
])
console.log(m1.size) // 3

// 使用自定义迭代器初始化映射
const m2 = new Map({
  [Symbol.iterator]: function*() {
    yield ['key1', 'val1'],
    yield ['key1', 'val1'],
    yield ['key1', 'val1'],
  }
})
console.log(m2.size) // 3

// 映射期待的键值对,无论是否提供
const m3 = new Map([[]])
console.log(m3.has(undefined)) // true
console.log(m3.get(undefined)) // undefined

可以使用 set() 方法再添加键值对;可以使用 get() 和 has() 进行查询,可以通过 size 属性获取映射中的键值对的数量,还可以使用 delete() 和 clear() 删除值。

const m = new Map()
console.log(m.has('firstName')) // false
console.log(m.get('firstName')) // undefined
console.log(m.size) // 0

m.set('firstName', 'Matt').set('lastName', 'Frisbie')

console.log('firstName') // true
console.log(m.get('firstName')) // Matt
console.log(m.size) // 2

m.delete('firstName') // 只删除这一个键值对

console.log(m.has('firstName')) // false
console.log(m.has('lastName')) // true
console.log(m.size) // 1

m.clear() // 清除这个映射实例中的所有键/值对

console.log(m.has('firstName')) // false
console.log(m.has('lastName')) // false
console.log(m.size) // 0

set() 方法返回映射实例,因此可以把多个操作连缀起来,包括初始化声明:

const m = new Map().set('key1', 'val1')

m.set('key2', 'val2')
 .set('key3', 'key3')

console.log(m.size) // 3

Map 可以使用任何 JavaScript 数据类型作为键。Map 内部使用 SameValueZero 比较操作,基本上相当于使用严格对象相等的标准来检查键的匹配性。与 Object 类似,映射的值是没有限制的。

const m = new Map()
const functionKey = function() {}
const symbolKey = Symbol()
const objectKey = new Object()
m.set(functionKey, 'functionValue')
m.set(symbolKey, 'symbolValue')
m.set(objectKey, 'objectValue')
console.log(m.get(functionKey)) // functionValue
console.log(m.get(symbolKey)) // symbolValue
console.log(m.get(objectKey)) // objectValue
// SameValueZero 比较意味着独立实例不冲突
console.log(m.get(function() {})) // undefined

与严格相等一样,在映射中用作键和值的对象及其他“集合”类型,在自己的内容或属性被修改时仍然保持不变:

const m = new Map()
const objKey = {},
      objVal = {},
      arrKey = [],
      arrVal = []
m.set(objKey, objVal)
m.set(arrKey, arrVal)
objKey.foo = "foo"
objVal.bar = "bar"
arrKey.push("foo")
arrVal.push("bar")

console.log(m.get(objKey)) // {bar: "bar"}
console.log(m.get(arrKey)) // ["bar"]
// SameValueZero 比较也可能导致意想不到的冲突
const m = new Map()
const a = 0 / "", // NaN
      b = 0 / "", // NaN
      pz = +0,
      nz = -0;
console.log(a === b) // false
console.log(pz === nz) // true
m.set(a, "foo")
m.set(pz, "bar")
console.log(m.get(b)); // foo
console.log(m.get(nz)); // bar

# 顺序与迭代

与 Object 类型的一个主要差异是, Map 实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。

映射实例可以提供一个迭代器( Iterator ),能以插入顺序生成 [key, value] 形式的数组。可以通过 entries() 方法(或者 Symbol.iterator 属性,它引用 entries())取得这个迭代器:

const m = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"]
])
console.log(m.entries === m[Symbol.iterator]) // true
for (let pair of m.entries()) {
  console.log(pair)
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
for (let pair of m[Symbol.iterator]()) {
  console.log(pair)
}
// [key1,val1]
// [key2,val2]
// [key3,val3]

因为 entries() 是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:

const m = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"]
])
console.log([...m]) // [[key1,val1],[key2,val2],[key3,val3]]

如果没有传入数组的构造形式,也可以使用 forEach 去循环遍历:

const m = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"]
]);
m.forEach((val, key) => console.log(`${key} -> ${val}`))
// key1 -> val1
// key2 -> val2
// key3 -> val3


// keys() 和 values() 分别返回以插入顺序生成键和值的迭代器:
for (let key of m.keys()) {
  console.log(key)
}
// key1
// key2
// key3
for (let key of m.values()) {
  console.log(key)
}
// value1
// value2
// value3

键和值在迭代器遍历时是可以修改的,但是作为键的字符串是不可以修改的,键的对象的属性是可以修改的。

const m1 = new Map([
  ['key1', 'val1']
])
// 作为键的字符串原始值是不能修改的
for (let key of m1.keys()) {
  key = 'newKey'
  console.log(key) // newKey
  console.log(m1.get('key1')) // val1
}
const keyObj = {id: 1}
const m = new Map([
  [keyObj, 'val1']
]);
// 修改了作为键的对象的属性,但对象在映射内部仍然引用相同的值
for (let key of m.keys()) {
  key.id = 'newKey'
  console.log(key) // {id: "newKey"}
  console.log(m.get(keyObj)) // val1
}
console.log(keyObj) // {id: "newKey"}

# 选择 Object 还是 Map

  1. 内存占用 Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。不同浏览器的情况不同,但给定固定大小的内存, Map 大约可以比 Object 多存储 50%的键/值对。
  2. 插入性能 向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然 Map 的性能更佳。
  3. 查找速度 与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。
  4. 删除性能 使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null 。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的 delete() 操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择 Map 。

# WeakMap

弱映射(WeakMap)是一种新的集合类型。

# 基本 API

弱映射中的键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出TypeError 。值的类型没有限制。

const key1 = {id: 1},
      key2 = {id: 2},
      key3 = {id: 3}
// 使用嵌套数组初始化弱映射
const wm1 = new WeakMap([
  [key1, 'val1'],
  [key2, 'val2'],
  [key3, 'val3']
])
console.log(wm1.get(key1)) // val1
console.log(wm1.get(key2)) // val2
console.log(wm1.get(key3)) // val3
// 初始化是全有或全无的操作
// 只要有一个键无效就会抛出错误,导致整个初始化失败
const wm2 = new WeakMap([
  [key1, 'val1'],
  ['BADKEY', 'val2'],
  [key3, 'val3']
])
// TypeError: Invalid value used as WeakMap key
typeof wm2
// ReferenceError: wm2 is not defined
// 原始值可以先包装成对象再用作键
const stringKey = new String('key1')
const wm3 = new WeakMap([
  stringKey, 'val1'
]);
console.log(wm3.get(stringKey)) // "val1"

可以使用 set() 添加键值,has() 和 get() 查询,delete() 删除。

const wm = new WeakMap()
const key1 = {id: 1},
      key2 = {id: 2}
console.log(wm.has(key1)) // false
console.log(wm.get(key1)) // undefined

wm.set(key1, 'Matt')
  .set(key2, 'Frisbie')
console.log(wm.has(key1)) // true
console.log(wm.get(key1)) // Matt

wm.delete(key1) // 只删除这一个键/值对
console.log(wm.has(key1)) // false
console.log(wm.has(key2)) // true


const key3 = {id: 3}
const wm1 = new WeakMap().set(key1, 'val1')
wm1.set(key2, 'val2')
   .set(key3, 'val3')
console.log(wm1.get(key1)) // val1
console.log(wm1.get(key2)) // val2
console.log(wm1.get(key3)) // val3

# 弱键

WeakMap 键不属于正式的引用,不会阻止垃圾回收。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。比如:

const wm = new WeakMap()
wm.set({}, 'val')

set() 方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身也会成为垃圾回收的目标。

const wm = new WeakMap()
const container = {
  key: {}
}
wm.set(container.key, 'val')
function removeReference() {
  container.key = null
}

上面代码, container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。不过,如果调用了 removeReference() ,就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉。

# 不可迭代键

因为 WeakMap 中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。当然,也用不着像 clear() 这样一次性销毁所有键/值的方法。

WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。

# 使用弱映射

  1. 私有变量

可以使用弱映射,实现私有变量;私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。

const wm = new WeakMap()
class User {
  constructor(id) {
    this.idProperty = Symbol('id')
    this.setId(id)
  }
  setPrivate(property, value) {
    const privateMembers = wm.get(this) || {}
    privateMembers[property] = value
    wm.set(this, privateMembers)
  }
  getPrivate(property) {
    eturn wm.get(this)[property]
  }
  setId(id) {
    this.setPrivate(this.idProperty, id)
  }
  getId() {
    return this.getPrivate(this.idProperty)
  }
}
const user = new User(123)
console.log(user.getId()) // 123
user.setId(456)
console.log(user.getId()) // 456
// 并不是真正私有的
console.log(wm.get(user)[user.idProperty]) // 456

上面代码只需要对象实例的引用和弱映射,就可以取得“私有”变量了。为了避免这种访问,可以用一个闭包把 WeakMap 包装起来,这样就可以把弱映射与外界完全隔离开了:

const User = (() => {
  const wm = new WeakMap()
  class User {
    constructor(id) {
      this.idProperty = Symbol('id')
      this.setId(id)
    }
    setPrivate(property, value) {
      const privateMembers = wm.get(this) || {}
      privateMembers[property] = value
      wm.set(this, privateMembers)
    }
    getPrivate(property) {
      return wm.get(this)[property]
    }
    setId(id) {
      this.setPrivate(this.idProperty, id)
    }
    getId(id) {
      return this.getPrivate(this.idProperty)
    }
  }
  return User
})()
const user = new User(123)
alert(user.getId()) // 123
user.setId(456)
alert(user.getId()) // 456
  1. DOM 节点元数据 因为 WeakMap 实例不会妨碍垃圾回收,所以非常适合保存关联元数据。 当节点从 DOM 树中被删除后,垃圾回收程序就可以立即释放其内存(假设没有其他地方引用这个对象):
const m = new WeakMap()
const loginButton = document.querySelector('#login')
// 给这个节点关联一些元数据
m.set(loginButton, {disabled: true})

# Set

# 基本 API

// 创建实例
const m = new Set()

// 使用数组初始化集合
const s1 = new Set(["val1", "val2", "val3"])
console.log(s1.size) // 3
// 使用自定义迭代器初始化集合
const s2 = new Set({
[Symbol.iterator]: function*() {
  yield "val1";
  yield "val2";
  yield "val3";
  }
})
console.log(s2.size) // 3

初始化之后,可以使用 add() 增加值,使用 has() 查询,通过 size 取得元素数量,以及使用 delete() 和 clear() 删除元素:

const s = new Set();
console.log(s.has("Matt")); // false
console.log(s.size); // 0
s.add("Matt")
 .add("Frisbie");
console.log(s.has("Matt")); // true
console.log(s.size); // 2
// delete() 返回一个布尔值,表示集合中是否存在要删除的值
s.delete("Matt"); // true
console.log(s.has("Matt")); // false
console.log(s.has("Frisbie")); // true
console.log(s.size); // 1
s.clear(); // 销毁集合实例中的所有值
console.log(s.has("Matt")); // false
console.log(s.has("Frisbie")); // false
console.log(s.size); // 0

// add() 返回集合的实例,所以可以将多个添加操作连缀起来,包括初始化:
const s = new Set().add("val1");
s.add("val2")
 .add("val3");
console.log(s.size); // 3

与 Map 类似, Set 可以包含任何 JavaScript 数据类型作为值。与严格相等一样,用作值的对象和其他“集合”类型在自己的内容或属性被修改时也不会改变:

const s = new Set();
const objVal = {},
arrVal = [];
s.add(objVal);
s.add(arrVal);
objVal.bar = "bar";
arrVal.push("bar");
console.log(s.has(objVal)); // true
console.log(s.has(arrVal)); // true

# 顺序与迭代

Set 会维护值插入时的顺序,因此支持按顺序迭代;可以提供一个迭代器,可以通过 values() 方法及其别名方法 keys() (或者 Symbol.iterator 属性,它引用 values() )进行迭代:

const s = new Set(["val1", "val2", "val3"]);
console.log(s.values === s[Symbol.iterator]); // true
console.log(s.keys === s[Symbol.iterator]); // true
for (let value of s.values()) {
  console.log(value);
}
// val1
// val2
// val3
for (let value of s[Symbol.iterator]()) {
  console.log(value);
}
// val1
// val2
// val3

// 因为 values() 是默认迭代器,所以可以直接对集合实例使用扩展操作,把集合转换为数组:
console.log([...s]); // ["val1", "val2", "val3"]

评 论:

更新: 12/27/2020, 4:59:16 PM