Array & 定型数组

401 阅读11分钟

Array

Array类型表示一组有序的值,并提供了操作和转换值的能力。

ECMAScript数组是动态大小的,每个元素都可以存储任意类型的数据。

创建数组

  • 使用Array构造函数

    let colors = new Array();

    new操作符可省略:let colors = Array();

    如果给Array()传入一个数值,则会识别为length属性: let colors = new Array(3);,意思是创建一个长度为3的数组

    如果传入的是多个值,则会识别为数组元素:let colors = new Array('red', 'blue', 'green');。如果传入的是多个数值,也会识别为数组元素

  • 数组字面量

    let colors = ['red', 'blue'];

与对象一样,在使用数组字面量表示法创建数组不会调用Array构造函数。

Array构造函数还有两个ES6新增的用于创建数组的静态方法:

from()

用于将类数组结构转换为数组实例,对现有数组执行浅复制

// 字符串会被拆分为单字符数组
console.log(Array.from('Matt')); // ['M', 'a', 't', 't']

// 将集合和映射转换为一个新数组
const m = new Map().set(1,2);
const s = new Set().add(1).add(2);
console.log(Array.from(m)); // [ [1, 2] ]
console.log(Array.from(s)); // [1, 2]

// 可使用任何可迭代对象
const iter = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
    }
};
console.log(Array.from(iter)); // [1, 2]

// 将arguments对象转换为数组
function getArgs() {
    return Array.from(arguments);
}
console.log(getArgs(1, 2)); // [1, 2]

// 转换带有必要属性的自定义对象
const likeObj = {
    0: 1,
    1: 2,
    length: 2
}
console.log(Array.from(likeObj)); // [1, 2]

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

const a3 = Array.from([1, 2], function(x) { return x**this.exponent }, { exponent: 2 });
console.log(a3); // [1, 4]

of()

用于将一组参数转换为数组。这个方法用于替代在ES6之前常用的Array.prototype.slice.call(arguments)

console.log(Array.of(1, 2)); // [1, 2]

通过修改length属性,可以从数组末尾删除或添加元素。

let colors = ['red', 'blue', 'green'];
colors.length = 2;
console.log(colors[2]); // undefined

colors.length = 4;
console.log(colors[3]); // undefined

检测数组

  • instanceof

    使用instanceof的问题是假定只有一个全局执行上下文。

    如果网页里有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的Array构造函数。如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。

  • Array.isArray()

    这个方法的目的就是确定一个值是否为数组,而不管它是在哪个全局执行上下文中创建的。

迭代器方法

在ES6中,Array的原型上暴露了3个用于检索数组内容的方法:

  • keys()

    返回数组索引的迭代器

  • values()

    返回数组元素的迭代器

  • entries()

    返回索引/值对的迭代器

因为这些方法都返回迭代器,所以可以将他们的内容通过Array.from()直接转换为数组实例。

复制和填充方法(ES6新增)

copyWithin()

会按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置。

let arrs = [0, 1, 2, 3, 4, 5, 6];

// 复制索引0开始的内容,插入到索引5开始的位置。在源索引或目标索引到达数组边界时停止
arrs.copyWithin(5);
console.log(arrs); // [0, 1, 2, 3, 4, 0, 1]

// 从arrs中复制索引5开始的内容,插入到索引0开始的位置
let arrs = [0, 1, 2, 3, 4, 5, 6];
arrs.copyWithin(0, 5);
console.log(arrs); // [5, 6, 2, 3, 4, 5, 6]

// 从arrs中复制索引0开始到索引3结束的内容,插入到索引4开始的位置
let arrs = [0, 1, 2, 3, 4, 5, 6];
arrs.copyWithin(4, 0, 3); // [0, 1, 2, 3, 0, 1, 2]
console.log(arrs);

copyWithin()静默忽略超出数组边界、零长度及方向相反的索引范围。

fill()

填充数组,向一个已有的数组中插入全部或部分相同的值。

const a = [1, 2, 3, 4];

a.fill('a');
console.log(a); // ['a', 'a', 'a', 'a']

// 用'b'填充索引大于等于2的元素
a.fill('b', 2);
console.log(a); // ['a', 'a', 'b', 'b']

// 用'c'填充索引大于等于2且小于3的元素
a.fill('c', 2, 3);
console.log(a); // ['a', 'a', 'c', 'b']

fill()静默忽略超出数组边界、零长度及方向相反的索引范围

toString()

返回由数组中每个值的等效字符串拼接而成的一个逗号分割的字符串

let colors = ["red", "blue", "green"];
console.log(colors.toString()); // red,blue,green

join() 接收分隔符将数组拼接成字符串

如果数组中某一项是nullundefined,则在join()toLocaleString()toString()valueOf()返回的结果中会以空字符串表示。

栈方法

push()

接收任意数量的参数,并将他们添加到数组末尾,返回数组的最新长度。

pop()

用于删除数组的最后一项,同时减少数组的length值,返回被删除的项。

队列方法

shift()

删除数组的第一项并返回它,数组长度减1.

unshift()

在数组开头添加任意多个值,然后返回新的数组长度。

排序方法

以下两个方法都返回它们的数组的引用

reverse()

将数组元素反向排列

sort()

按照升序重新排列数组元素,即最小的值在前面,最大的值在后面

在每一项上调用String()转型函数,然后比较字符串来决定顺序

为此,sort()方法接收一个比较函数,用于自定义排序

操作方法

concat()

首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组。原始数组不变

可以通过[Symbol.isConcatSpreadable]阻止concat()打平参数数组:

let colors = ['red', 'green', 'blue'];
let newColors = ['black', 'brown'];
let moreNewColors = {
    [Symbol.isConcatSpreadable]: true,
    length: 2,
    0: 'pink',
    1: 'cyan'
};
newColors[Symbol.isConcatSpreadable] = false;
// 强制不打平数组
let colors2 = colors.concat('yellow', newColors);
// 强制打平类数组对象
let colors3 = colors.concat(moreNewColors);
console.log(colors2); // ['red', 'green', 'blue', 'yellow', ['black', 'brown']]
console.log(colors3); // ['red', 'green', 'blue', 'pink', 'cyan']

slice(startIdx\[, endIdx])

用于创建一个包含原有数组中一个或多个元素的新数组。

如果只有一个参数,则返回该索引到数组末尾的所有元素。

endIdx不包含结束索引对应的元素

此操作不影响原始数组

如果参数有负值,则以数组长度加上这个负值的结果确定位置。

比如,在包含5个元素的数组上调用slice(-2, -1),就相当于调用slice(3, 4)

如果结束位置小于开始位置,则返回空数组

splice()

使用它的方式可以有很多种。主要目的是在数组中插入元素。但有3种不同的方式使用这个方法

始终返回从数组中被删除的元素(如果没有删除元素,则返回空数组)

  • 删除:splice(deleteIdx, deleteLength)

    可以从数组中删除任意多个元素,比如splice(0, 2)会删除数组前两个元素

  • 插入: splice(startIdx, 0, insertObj)

    可以在数组中指定的位置插入元素。第三个参数之后还可以传入第四个、第五个参数,乃至任意多个要插入的元素。比如splice(2, 0, 'red', 'green')会从数组位置2开始插入两个元素

  • 替换:splice(startIdx, deleteLength, insertObj)

    在删除元素的同时可以指定位置插入新元素。比如splice(2, 1, 'red', 'green')会在位置2删除一个元素,然后从该位置开始向数组中插入两个元素

搜索和位置方法

  • 按严格相等搜索

indexOf(obj\[, startSearchIdx])

lastIndexOf(obj\[, startSearchIdx])

includes() -- ES7新增

  • 按断言函数搜索

    断言函数的返回值决定了响应索引的元素是否被认为匹配

find((ele, idx, array) => ele.age < 20)

返回第一个匹配的元素

findIndex((ele, idx, array) => ele.age < 20)

返回第一个匹配元素的索引

迭代方法

以下方法都不会改变调用他们的数组

every()

每一项都满足,则返回true

filter()

筛选符合条件的数组元素

forEach()

对数组中每一项执行传入的函数,没有返回值

map()

对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组

some()

如果有一项满足,则返回true

归并方法

以下方法迭代数组的所有项,并在此基础上构建一个最终返回值。

reduce((prev, cur, idx, array) => prev + cur, initVal)

从数组第一项开始遍历到最后一项

reduceRight((prev, cur, idx, array) => prev + cur, initVal)

从最后一项开始遍历至第一项

定型数组

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

定型数组包含一套不同的引用类型,用于管理数值在内存中的类型

历史

随着浏览器的流行,人们开始通过它来运行复杂的3D应用程序。

浏览器提供商实验性地在浏览器中增加了用于渲染复杂图形应用程序的编程平台,其目标是开发一套JS API,从而充分利用3D图形API和GPU加速,以便在canvas上渲染复杂图形。

最后的JS API是基于OpenGL ES(OpenGL专注于2D和3D计算机图形的子集),这个新API被命名为WebGL,它会被兼容WebGL的浏览器原生解释执行。

在WebGL早期版本中,因为JS数组与原生数组之间不匹配,所以出现了性能问题。图形驱动程序API通常不需要以JS默认双精度浮点格式传递给他们的数值,而这恰恰是JS数组在内存中的格式。因此每次WebGL与JS运行时 之间传递数组时,WebGL绑定都需要在目标环境分配新数组,以其当前格式迭代数组,然后将数值转型为新数组中的适当格式,而这些都需要花费很多时间

Mozilla为解决这个问题实现了CanvasFloatArray,最终,CanvasFloatArray变成了Float32Array,也就是今天定型数组中可用的第一个“类型”。

ArrayBuffer

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

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

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

const buf = new ArrayBuffer(16); // 在内存中分配16字节
console.log(buf.byteLength); // 16

ArrayBuffer一经创建就不能再调整大小。不过,可以使用slice()复制其全部或部分到一个新实例中。

ArrayBuffer在分配失败时会抛出错误,声明后会将所有二进制位初始化为0。

通过声明ArrayBuffer分配的堆内存可以被当成垃圾回收,不用手动释放。

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

DataView

第一种允许读写ArrayBuffer的视图是DataView。这个视图专为文件I/0和网络I/0设计,其API支持对缓冲数据的高度控制,但相比于其他类型的视图性能也差一些

DataView对缓冲内容没有任何预设,也不能迭代

必须在对已有的ArrayBuffer读取或写入时才能创建DataView实例。

const buf = new ArrayBuffer(16);

// DataView默认(只有一个参数时)使用整个ArrayBuffer
// 第二个参数表示视图从缓冲起点开始
// 第三个参数表示限制视图为前8个字节,如果不指定,则使用剩余缓冲
const fullDataView = new DataView(buf, 0, 8);

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

  • 首先是要读或写的字节偏移量。可以看成DataView中的某种“地址”。

  • DataView应该使用ElementType来实现JS的Number类型到缓冲内二进制格式的转换

ElementType

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

  • 最后是内存中值的字节序。默认为大端字节序

字节序

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

DataView只支持两种约定:

  • 大端字节序, 也称为“网络字节序”,意思是最高有效位保存在第一个字节,而最低有效位保存在最后一个字节

  • 小端字节序,与大端字节序相反

JS运行时所在系统的原生字节序决定了如何读取或写入字节。对一段内存而言,DataView是一个中立接口,它会遵循你指定的字节序(接收一个可选地布尔值,设置为true即可启用小端字节序)

定型数组

定型数组是另一种形式的ArrayBuffer视图,虽然概念上与DataView接近,但定型数组的区别在于,它特定于一种ElementType且遵循系统原生的字节序。

定型数组提供了适用面更广的API和更高的性能。

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