集合引用类型

  • 对象
  • 数组与定型数组

Object

Object 是 ECMAScript 中最常用的类型之一。

创建 Object

  1. 使用 new 操作符 和 Object 构造函数
  2. 使用对象字面量表示法
// 1
let p = new Object()
p.name = 'dan'

// 2
let per = {
  name: 'dan'
}

表达式上下文,指的是期待返回值的上下文。

在使用对象字面量表示法定义对象时,并不会实际调用 Object 构造函数。

存取属性

  1. 点语法
  2. 中括号
let person = {
  name: 'dan'
}

console.log(person.name)    // dan
console.log(person['name']) // dan

Array

ECMAScript 中数组是动态大小的,会随着数据添加而自动增长。

创建数组

  1. 使用 Array 构造函数
let arr = new Array()

// 传一个数值参数
let cols = new Array(20) // length 为 20

// 传一组字符串
let nums = new Array('1', '2', '3') // ['1', '2', '3']
  1. 使用数组字面量表示法

数组字面量是在中括号中包含以逗号分隔的元素列表。

let colors = ['red', 'green', 'blue']
let nums = []
let vals = [1,2,]

使用数组字面量表示法创建数组不会调用 Array 构造函数。

  1. 使用 ES6 新增的用于创建数组的静态方法

from() 和 of()

Array.from(arrayLike[, mapFn[, thisArg]])

#arrayLike

An array-like or iterable object to convert to an array.

#mapFn

Optional. Map function to call on <every element of the array>.

#thisArg

Optional. Value to use as this when executing mapFn.

// from() 将字符串拆分为单字符数组
console.log(Array.from('Marry')) // ['M','a','r','r','y']

// 使用 from() 将集合和映射转换为一个新数组
const m = new Map().set(1,2).set(3,4)
const s = new Map().set(1).set(2).set(3).set(4)

console.log(Array.from(m)) // [[1,2],[3,4]]
console.log(Array.from(s)) // [1,2,3,4]

// 对现有数组进行浅拷贝
const a1 = [1,2,3,4]
const a2 = Array.from(a1)

console.log(Array.from(a1)) // [1,2,3,4]
console.log(a1 === a2)      // false

// 可以使用任何迭代对象
const iter = {
  *[Symbol.iterator]() {
    yield 1
    yield 2
    yield 3
    yield 4
  }
}

console.log(Array.from(iter)) // [1,2,3,4]

// arguments 对象可以被轻松地转换为数组
function getArgsArray() {
  return Array.from(arguments)
}

console.log(getArgsArray(1,2,3,4)) // [1,2,3,4]

// 转换类数组对象[具有必要属性的自定义对象]
const arrayLickObject = {
  0: 1,
  1: 2,
  2: 3,
  3: 4,
  length: 4
}

console.log(Array.from(arrayLikeObject)) // [1,2,3,4]

Array.from() 还可以接收第二个可选的映射函数参数。 这个函数可以直接增强新数组的值,无须使用 Array.from().map() 先创建中间数组。还可以接受第三个可选参数,用于指定映射函数中 this 的值。但不适用于箭头函数。

const a1 = [1,2,3,4]
const a2 = Array.from(a1, x => x**2)
const a3 = Array.from(a1, function(x) {return x**this.exponent}, { exponent: 2 })
console.log(a2) // [1,4,9,16]
console.log(a3) // [1,4,9,16]

Array.of() 可以把一组参数转换为数组。这个方法用于替代在ES6之前的 Array.prototype.slice.call(argements) 写法。

console.log(Array.of(1,2,3,4)) // [1,2,3,4]
console.log(Array.of(undefined)) // [undefined]

数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位。ECMAScript 会将逗号之间相应索引位置的值当成空位,ES6规范重新定义了该如何处理这些空位。

const arr = [,,,,,]     // 5个逗号,创建包含5个元素的空位数组
console.log(arr.length) // 5
console.log(arr)        // [empty x 5]

ES6 新增的方法和迭代器普遍将这些空位当成存在的元素,不过值为 undefined

const arr = [1,,,,5]

for (const tmp of arr) {
  console.log(option === undefined)
}

// false
// true
// true
// true
// false

const a = Array.from([,,,]) // 创建包含 3 个空位的数组
for(const val of a) {
  console.log(val === undefined)
}

// true * 3

console.log(Array.of(...[,,,])) // [undefined, undefined, undefined]

ES6 之前的方法会忽略这个空位

map() 会跳过空位

callback is invoked only for indexes of the array which have assigned values, including undefined. It is not called for missing elements of the array.

for...in 会跳过

Object.keys() 会跳过

forEach(), filter(), every() 和 some() 都会跳过空位

const opt = [1,,,,5]
console.log(opt.map(() => 6)) // [6, undefined, undefined, undefined, 6]

for(const o in opt) {
  console.log(o)
}

Object.keys(opt) // ['0', '4']

ES6则是明确将空位转为 undefined

由于空位的处理规则非常不统一,所以建议避免出现空位。

数组索引

使用中括号并提供相应值的数字索引。

数组的属性 length,会根据数据内容变化后的最大位置,再加上 1。

let a = []
a[100] = 1
console.log(a.length) // 101

当主动给一个数组的 length 属性赋值时,数组会根据 length 的大小,扩展或缩短数组大小。当 length 设置为大于数组元素数的值时,新添加的元素都以 undefined 填充。当 length 设置为小于数组元素数的值时,删除多余的元素。

检测数组

判断一个对象是不是数组。

instanceof 操作符判断 只存在一个全局作用域的情况下。

ECMAScript 提供了 Array.isArray() 方法,解决了如何确定一个值是否为数组,不用管在哪个全局执行上下文创建的。

迭代器方法

ES6 中,Array 的原型上暴露了 3 个用于检索数组内容的方法:keys()、values()、entries()。

keys() 返回数组索引的迭代器。

values() 返回数组元素的迭代器。

entries() 返回索引/值对的迭代器。

const a = ['foo', 'bar', 'baz', 'qux']

// 迭代器可以使用 Array.from() 直接转换成数组实例
const aKeys = Array.from(a.keys())
const aValues = Array.from(a.values())
const aEntries = Array.from(a.entries())

console.log(aKeys)     // [0, 1, 2, 3]
console.log(aValues)   // ["foo", "bar", "baz", "qux"]
console.log(aEntries)  // [[0, "foo"], [1, "bar"], [2, "baz"], [3, "qux"]]

其余方法详见后续: MDN 系列之 Array

定型数组

定型数组是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率。

历史

  1. WebGL

最后的 JavaScript API 是基于 OpenGL ES 2.0 规范的。 OpenGL ES 是 OpenGL 专注于 2D 和 3D 计算机图形的子集。这个新 API 被命名为 WebGL。

早期版本中,因为 JavaScript 数组与原生数组之间不匹配,所以出现了性能问题。JavaScript 数组在内存中默认双精度浮点格式,但 WebGL 并不需要。因此,每次 WebGL 与 JavaScript 运行时之间传递数组时,WebGL 绑定都需要在目标环境分配心数组,以其当前格式迭代数组,然后将数组转型为新数组中的适当格式,这相当耗时。

  1. 定型数组

为了解决这个问题,Mozilla 实现了 CanvasFloatArray。这是一个提供 JavaScript 接口的、C 语言风格的浮点值数组。JavaScript 运行时使用这个类型可以分配、读取和写入数组。这个数组可以直接传给底层图形驱动程序 API,也可以直接从底层获取到。最终,CanvasFloatArray 变成了 Float32Array。

ArrayBuffer

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

ArrayBuffer 是所有定型数组及视图引用的基本单位。

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

const buf = new ArrayBuffer(16)
console.log(buf.byteLength) // 16

ArrayBuffer 一经创建就不能再调整大小,可以使用 slice() 复制部分或全部到一个新的实例中。类似于 C++ 的 malloc()。

  • ArrayBuffer 分配失败时会抛出错误。
  • 分配内存不能超过 Number.MAX_SAFE_INTEGER 字节。
  • 声明 ArrayBuffer 则会将所有二进制位初始化为 0。
  • 通过声明 ArrayBuffer 分配的堆内存可以被当成垃圾回收,不需要手动释放。

DataView

略过。

字节序,指的是计算系统维护的一种字节顺序的约定。

ElementType,ECMAScript 6 支持8中不同的 ElementType

ElementType 字节 说明 值的范围
Int8 1 8位有符号整数 -128~127
Uint8 1 8位无符号整数 0~255
Int16 2 16位有符号整数 -32768~32767
Uint16 2 16位无符号整数 0~65535
Int32 4 32位有符号整数 -2147483648~2147483647
Uint32 4 32位无符号整数 0~4294967295
Float32 4 32位IEEE-754浮点数 -3.4e+38~+3.4e+38
Float64 8 64位IEEE-754浮点数 -1.7e+308~+1.7e+308

定型数组

定型数组是一种 ArrayBuffer 视图。特定于一种 ElementType 且遵循系统原生的字节序。提供了适用面更广的 API 和更高的性能。

设计定型数组的目的就是提高与 WebGL 等原生库交换二进制数据的效率。

由于定型数组的二进制表示对操作系统而言是一种容易使用的格式,JavaScript 引擎可以重度优化算术运算、按位运算和其他对定型数组的常见操作,因此使用它们速度极快。

创建定型数组的方式

  • 读取已有的缓冲
  • 使用自有缓冲
  • 填充可迭代结构
  • 填充基于任意类型的定型数组
  • 通过 <ElementType>.from() 和 <ElementType>.of() 创建
// 创建一个 12 字节的缓冲
const buf = new ArrayBuffer(12)

// 创建一个引用该缓冲的 Int32Array
const ints = new Int32Array(buf)
// 这个定型数组知道自己的每个元素需要4字节
// 因此,ints 的长度为 3
console.log(ints.length) // 3

// 创建一个长度为 6 的 Int32Array
const ints2 = new Int32Array(6)

// 每个数值使用 4 字节,因此 ArrayBuffer 是 24 字节
console.log(ints2.length) // 24

// 定型数组指向关联缓冲的引用
console.log(ints2.bufffer.byteLength) // 24

// 创建一个包含 [2,4,6,8] 的 Int32Array
const ints3 = new Int32Array([2,4,6,8])
console.log(ints3.length) // 4
console.log(ints3.buffer.byteLength) // 16
console.log(ints3[2]) // 6

// 通过复制 ints3 的值创建一个 Int16Array
const ints4 = new Int16Array(ints3)
console.log(ints4.length) // 4
console.log(ints4.buffer.byteLength) // 8
console.log(ints4[2]) // 6

// 基于普通数组来创建一个 Int16Array
const ints5 = new Int16Array.from([3,5,7,9])
console.log(ints5.length) // 4
console.log(ints5.buffer.byteLength) // 8
console.log(ints5[2]) // 7

定型数组的构造函数和实例都有一个 BYTES_PER_ELEMENT 属性,返回该类型数组中每个元素的大小。

如果定型数组没有用任何值初始化,则其关联的缓冲就会以0填充

定型数组支持如下操作符、方法、属性:

  • []
  • copyWithin()
  • entries()
  • every()
  • fill()
  • filter()
  • find()
  • findIndex()
  • forEach()
  • indexOf()
  • join()
  • keys()
  • lastIndexOf()
  • length
  • map()
  • reduce()
  • reduceRight()
  • reverse()
  • slice()
  • some()
  • sort()
  • toLocaleString()
  • toString()
  • values()

定型数组有一个 Symbol.iterator 符号属性,因此可以使用 for...of 循环和扩展操作符来操作。

以下方法不适用于定型数组❌:

  • concat()
  • pop()
  • push()
  • shift()
  • splice()
  • unshift()

定型数组提供了两个方法,可以快速向外或向内复制数据: set() 和 subarray()。

set() 从提供的数组或定型数组中把值复制到当前定型数组中指定的索引位置。

// 创建长度为 8 的 int16 数组
const container = new Int16Array(8)

// 把定型数组复制为前 4 个值
// 偏移量默认为索引0
container.set(Int8Array.of(1, 2, 3, 4))
console.log(container)
// [1,2,3,4,0,0,0,0]

// 把普通数组复制为后 4 个值
// 偏移量 4 表示从索引 4 开始插入
container.set([5,6,7,8], 4)
console.log(container)
// [1,2,3,4,5,6,7,8]

// 溢出会抛出错误
container.set([5,6,7,8], 7)
// RangeError

subarray() 执行与 set() 相反的操作,会从基于原始定型数组中复制的值返回一个新定型数组,复制值时的开始索引和结束索引是可选的。

const source = Int16Array.of(2, 4, 6, 8)
// 把整个数组复制为一个同类型的新数组
const fullCopy = source.subarray()
console.log(fullCopy)
// [2, 4, 6, 8]

// 从索引 2 开始复制数组
const halfCopy = source.subarray(2)
console.log(halfCopy)
// [6, 8]

// 从索引1开始复制到索引3
const partialCopy = source.subarray(1, 3)
console.log(partialCopy)
// [4, 6]

定型数组没有原生的拼接能力,需要手动构建。

定型数组中值的上溢和下溢不会影响到其他索引,但仍然需要考虑数组的元素应该是什么类型。