深入理解ES6--10.增强的数组功能

1,486 阅读15分钟

原创文章&经验总结&从校招到A厂一路阳光一路沧桑

详情请戳www.codercc.com

主要知识点:创建数组、数组上的新方法、类型化数组

1. 创建数组

在 ES6 之前创建数组主要存在两种方式: Array 构造器与数组字面量写法。这两种方式都需要将数组的项分别列出,并且还要受到其他限制。将“类数组对象”(即:拥有数值类型索引与长度属性的对象) 转换为数组也并不自由,经常需要书写额外的代码。为了使数组更易创建,ES6 新增了Array.of()Array.from() 方法。

Array.of()方法

Array.of()方法会将方法的传入参数全部作为数组里的数据内容,而不管参数的数量与类型:

let items = Array.of(1,'2');
console.log(items); //Array(2)
console.log(items.length); //2

Array.from()方法

可使用Array.from()方法可以将类数组对象和可迭代对象转换成数组。该方法接收三个参数:

  1. 待转换的可迭代对象或者类数组对象(具有数值索引和长度属性的对象);
  2. 可选的映射函数;
  3. 指定映射函数内部的 this 值;

映射转换

如果你想实行进一步的数组转换,你可以向 Array.from() 方法传递一个映射用的函数作为第二个参数。此函数会将类数组对象的每一个值转换为目标形式,并将其存储在目标数组的对应位置上。例如:

function trans(values){
	return Array.from(arguments,item=>item+1);
}

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

可迭代对象上使用

Array.from() 方法不仅可用于类数组对象,也可用于可迭代对象,也就是说可以将任意包含 Symbol.iterator 属性的对象转换成数组,例如:

let obj = {
	*[Symbol.iterator](){
		yield 1;
		yield 2;
		yield 3;
	}
}

console.log(Array.from(obj)); //[1, 2, 3]

2. 数组上所有的新方法

2.1 find()和findIndex()方法

indexOf()lastIndexOf()方法用于查找特定值在数组中的位置,而如果需要查找在数组中满足特定条件的元素就需要使用find()findIndex()方法。

find() 方法和 findIndex() 方法均接受两个参数:一个是回调函数、一个是可选的指定回调函数中的 this 值。回调函数中有三个参数:

  1. 数组中的数据项元素;
  2. 元素在数组中的索引位置;
  3. 数组实例对象本身;

回调函数与 map()forEach() 等方法的回调函数中的参数一致。例如,需要找到在数组中大于5的元素:

let arr = Array.of(1,5,8,9);

console.log(arr.find(item=>item>5)); //8
console.log(arr.findIndex(item=>item>5)); //2

可以看出,find()方法返回的是满足特定条件的数据项,而 findIndex() 方法返回的是满足特定条件的元素索引。

2.2 fill()方法

fill()方法能使用特定值填充数组中的一个或多个元素。当只使用一个参数的时候,该方法会用该参数的值填充整个数组。若你不想改变数组中的所有元素,而只想改变其中一部分,那么可以使用可选的起始位置参数与结束位置参数(不包括结束位置的那个元素)

let arr = Array.of(1,5,8,9);

console.log(arr.fill(1,2,4)); //[1, 5, 1, 1]

若只给定起始位置,不指定结束位置的话,默认结束位置为数组末尾。

2.3 copyWithin()方法

copyWithin() 方法与 fill() 类似,可以一次性修改数组的多个元素。不过,与 fill() 使用单个值来填充数组不同, copyWithin() 方法允许你在数组内部复制自身元素。为此你需要传递两个参数给 copyWithin() 方法:从什么位置开始进行填充,以及被用来复制的数据的起始位置索引,以及可选的复制结束的位置(不包含该位置)

//copyWithin()
let arr = Array.of(1,5,8,9);
console.log(arr.copyWithin(2,0,2)); //[1, 5, 1, 5]

3. 类型化数组

ES6 采纳了类型化数组,将其作为语言的一个正式部分,以确保在 JS 引擎之间有更好的兼容性,并确保与 JS 数组有更好的互操作性。

3.1 数值数据类型

JS 数值使用 IEEE 754 标准格式存储,使用 64 位来存储一个数值的浮点数表示形式,该格式在 JS 中被同时用来表示整数与浮点数;当值改变时,可能会频繁发生整数与浮点数之间的格式转换。而类型化数组则允许存储并操作八种不同的数值类型:

  1. 8 位有符号整数(int8)
  2. 8 位无符号整数(uint8)
  3. 16 位有符号整数(int16)
  4. 16 位无符号整数(uint16)
  5. 32 位有符号整数(int32)
  6. 32 位无符号整数(uint32)
  7. 32 位浮点数(float32)
  8. 64 位浮点数(float64)

所有与类型化数组相关的操作和对象都围绕着这八种数据类型。为了使用它们,要先创建一个数组缓冲区用于存储数据。

3.2 数组缓冲区

数组缓冲区(array buffer) 是内存中包含一定数量字节的区域,而所有的类型化数组都基于数组缓冲区。创建数组缓冲区使用 ArrayBuffer 构造器:

let buffer = new ArrayBuffer(10);
console.log(buffer.byteLength); //10

调用 ArrayBuffer 构造器时,只需要传入单个数值用于指定缓冲区包含的字节数,而本例就创建了一个 10 字节的缓冲区。当数组缓冲区被创建完毕后,你就可以通过检查 byteLength属性来获取缓冲区的字节数。

还可以使用slice()方法来创建一个包含已有缓冲区部分内容的数组缓冲区,其中slice()方法可以使用起始位置和结束位置(不包含结束位置):

let buffer = new ArrayBuffer(10);
let buf = buffer.slice(4,8);
console.log(buf.byteLength); //4

仅仅创建一个数组缓冲器不能写入数据,是没有任何意义的。要想写入数据,需要创建视图(view)。

3.3 使用视图操作数组缓冲区

数组缓冲区代表了一块内存区域,而视图(views) 则是你操作这块区域的接口。视图工作在数组缓冲区或其子集上,可以读写某种数值数据类型的数据。 DataView 类型是数组缓冲区的通用视图,允许你对前述所有八种数值数据类型进行操作。

创建视图,需要使用 DataView() 构造器,可以指定可选参数-字节偏移量以及所要包含的字节数。如果不指定所要包含的字节数,则默认为从字节偏移量直到数组缓冲区的末尾。

let buffer = new ArrayBuffer(10),
view = new DataView(buffer, 5, 2);

此例中的 view 只能使用索引值为 5 与 6 的字节。使用这种方式,你可以在同一个数组缓冲区上创建多个不同的视图,这样有助于将单块内存区域供给整个应用使用,而不必每次在有需要时才动态分配内存。

获取视图信息

可以通过视图的可读属性来获取视图的信息:

  • buffer :该视图所绑定的数组缓冲区;
  • byteOffset :传给 DataView 构造器的第二个参数,如果当时提供了的话(默认值为0);
  • byteLength :传给 DataView 构造器的第三个参数,如果当时提供了的话(默认值为该缓冲区的 byteLength 属性。

读取或写入数据

对应于 JS 所有八种数值数据类型, DataView 视图的原型分别提供了在数组缓冲区上写入数据的一个方法、以及读取数据的一个方法。所有方法名都以“set”或“get”开始,其后跟随着对应数据类型的缩写。下面列出了能够操作 int8uint8 类型的读取/写入方法:

  • getInt8(byteOffset, littleEndian) :从 byteOffset 处开始读取一个 int8 值;
  • setInt8(byteOffset, value, littleEndian) :从 byteOffset 处开始写入一个 int8 值;
  • getUint8(byteOffset, littleEndian) :从 byteOffset 处开始读取一个无符号 int8 值;
  • setUint8(byteOffset, value, littleEndian) :从 byteOffset 处开始写入一个无符号int8 值。

get”方法接受两个参数:开始进行读取的字节偏移量、以及一个可选的布尔值,后者用于指定读取的值是否采用低字节优先方式(注:默认值为 false ) 。“set”方法则接受三个参数:开始进行写入的字节偏移量、需要写入的数据值、以及一个可选的布尔值用于指定是否采用低字节优先方式存储数据。

如果针对的是16位或者32位整数的话,只需要将上面的方法中的8,相应的改变成16或者32,就可以操作16位或者32值。

操作浮点数,提供了下面这些方法:

  • getFloat32(byteOffset, littleEndian) :从 byteOffset 处开始读取一个 32 位的浮点数;
  • setFloat32(byteOffset, value, littleEndian) :从 byteOffset 处开始写入一个 32 位的浮点数;
  • getFloat64(byteOffset, littleEndian) :从 byteOffset 处开始读取一个 64 位的浮点数;
  • setFloat64(byteOffset, value, littleEndian) :从 byteOffset 处开始写入一个 64 位的浮点数;

例如使用上述这些视图上的方法来进行操作:

let buffer = new ArrayBuffer(2),
view = new DataView(buffer);

view.setInt8(0,1);
console.log(view.getInt8(0)); //1

3.4 类型化数组就是视图

ES6 的类型化数组实际上也是针对数组缓冲区的特定类型视图,你可以使用这些数组对象来处理特定的数据类型,而不必使用通用的 DataView 对象。一共存在八种特定类型视图,对应着八种数值数据类型。类型化数组的构造器有:Int8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array。

创建特定类型视图

创建特定类型视图有三种方式:

  1. 第一种方式是使用与创建 DataView 时相同的参数,即:一个数组缓冲区、一个可选的字节偏移量、以及一个可选的字节数量;

  2. 第二种方式是传递单个数值给类型化数组的构造器,此数值表示该数组包含的元素数量(而不是分配的字节数) 。构造器将会创建一个新的缓冲区,分配正确的字节数以便容纳指定数量的数组元素,而你也可以使用 length 属性来获取这个元素数量;

  3. 第三种方式是向构造器传递单个对象参数,可以是下列四种对象之一:

    1. 类型化数组:数组所有元素都会被复制到新的类型化数组中。例如,如果你传递一个 int8类型的数组给 Int16Array 构造器,这些 int8 的值会被复制到 int16 数组中。新的类型化数组与原先的类型化数组会使用不同的数组缓冲区。
    2. 可迭代对象:该对象的迭代器会被调用以便将数据插入到类型化数组中。如果其中包含了不匹配视图类型的值,那么构造器就会抛出错误。
    3. 数组:该数组的元素会被插入到新的类型化数组中。如果其中包含了不匹配视图类型的值,那么构造器就会抛出错误。
    4. 类数组对象:与传入数组的表现一致。

    //第一种方式 let buffer = new ArrayBuffer(10), view1 = new Int8Array(buffer), view2 = new Int8Array(buffer, 5, 2);

    //第二种方式 let ints = new Int16Array(2), floats = new Float32Array(5);

    //第三种方式 let ints1 = new Int16Array([25, 50]), ints2 = new Int32Array(ints1);

3.5 类型化数组与常规数组的相似点

类型化数组在很多场景中都可以像常规数组那样被使用。例如,你可以使用 length 属性来获取类型化数组包含的元素数量,还可以使用数值类型的索引值来直接访问类型化数组的元素。它们二者具有的相似点:

  1. 公共方法

类型化数组拥有大量与常规数组等效的方法:

  • copyWithin()
  • entries()
  • fill()
  • filter()
  • find()
  • findIndex()
  • forEach()
  • indexOf()
  • join()
  • keys()
  • lastIndexOf()
  • map()
  • reduce()
  • reduceRight()
  • reverse()
  • slice()
  • some()
  • sort()
  • values()

相同的迭代器

与常规数组相同,类型化数组也拥有三个迭代器,它们是 entries() 方法、 keys()方法与values() 方法。这就意味着你可以对类型化数组使用扩展运算符,或者对其使用 for-of 循环,就像对待常规数组。

of()和from()方法

所有的类型化数组都包含静态的of()from() 方法,作用类似于 Array.of()Array.from() 方法。其中的区别是类型化数组的版本会返回类型化数组,而不返回常规数组。

3.5 类型化数组与常规数组的差异

二者最重要的区别就是类型化数组并不是常规数组,类型化数组并不是从 Array 对象派生的,使用 Array.isArray() 去检测会返回 false

行为差异

常规数组可以被伸展或是收缩,然而类型化数组却会始终保持自身大小不变。你可以对常规数组一个不存在的索引位置进行赋值,但在类型化数组上这么做则会被忽略。

let ints = new Int16Array([25, 50]);
console.log(ints.length); // 2
console.log(ints[0]); // 25
console.log(ints[1]); // 50
ints[2] = 5;
console.log(ints.length); // 2
console.log(ints[2]); // undefined

在本例中,尽管对索引值 2 的位置进行了赋值为 5 的操作,但 ints 数组却完全没有被伸展,数组的长度属性保持不变,所赋的值也被丢弃了。

类型化数组也会对数据类型进行检查以保证只使用有效的值,当无效的值被传入时,将会被替换为 0

let ints = new Int16Array(["hi"]);
console.log(ints.length); // 1
console.log(ints[0]); // 0

这段代码试图用字符串值 "hi" 创建一个 Int16Array ,而字符串对于类型化数组来说当然是无效的值,因此该字符串被替换为 0 并插入数组。此数组的长度仅仅是 1 ,而 ints[0]只包含了 0 这个值。

所有在类型化数组上修改项目值的方法都会受到相同的限制,例如当 map() 方法使用的映射函数返回一个无效值的时候,类型化数组会使用 0 来代替返回值

let ints = new Int16Array([25, 50]),
mapped = ints.map(v => "hi");
console.log(mapped.length); // 2
console.log(mapped[0]); // 0
console.log(mapped[1]); // 0
console.log(mapped instanceof Int16Array); // true
console.log(mapped instanceof Array); // false

由于字符串值 "hi" 并不是一个 16 位整数,它在结果数组中就被替换成为 0 。

遗漏的方法

尽管类型化数组拥有常规数组的很多同名方法,但仍然缺少了几个数组方法,包括下列这些:

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

附加的方法

类型化数组还有两个常规数组所不具备的方法: set() 方法与 subarray() 方法。这两个方法作用相反: set() 方法从另一个数组中复制元素到当前的类型化数组,而 subarray() 方法则是将当前类型化数组的一部分提取为新的类型化数组。

**set() 方法接受一个数组参数(无论是类型化的还是常规的) 、以及一个可选的偏移量参数,后者指示了从什么位置开始插入数据(默认值为 0 ) **。数组参数中的数据会被复制到目标类型化数组中,并会确保数据值有效。

let ints = new Int16Array(4);
ints.set([25, 50]);
ints.set([75, 100], 2);
console.log(ints.toString()); // 25,50,75,100

这段代码创建了一个包含四个元素的 Int16Array ;第一次调用 set() 复制了两个值到数组起始的两个位置;而第二次调用set() 则使用了一个值为 2 的偏移量参数,指明应当从数组的第三个位置(索引 2 ) 开始放置所复制的数据。

subarray() 方法接受一个可选的开始位置索引参数、以及一个可选的结束位置索引参数(像slice() 方法一样,结束位置的元素不会被包含在结果中) ,并会返回一个新的类型化数组。你可以同时省略这两个参数,从而创建原类型化数组的一个复制品。

let ints = new Int16Array([25, 50, 75, 100]),
subints1 = ints.subarray(),
subints2 = ints.subarray(2),
subints3 = ints.subarray(1, 3);
console.log(subints1.toString()); // 25,50,75,100
console.log(subints2.toString()); // 75,100
console.log(subints3.toString()); // 50,75

本例中利用 ints 数组创建了三个类型化数组。 subints1 数组是 ints 的一个复制品,包含了原数组的所有信息;而 subints2 则从原数组的索引 2 位置开始复制,因此包含了原数组的最末两个元素(即 75 与 100 ) ;最后的 subints3 数组值包含了原数组的中间两个元素,因为调用 subarray() 时同时使用了起始位置与结束位置参数。

4. 总结

  1. ES6 延续了 ES5 的工作以便让数组更加有用。新增了两种创建数组的方式: Array.of() 方法、以及 Array.from() 方法,其中后者可以将可迭代对象或类数组对象转换为正规数组;
  2. fill() 方法与 copyWithin() 方法允许你替换数组内的元素。 find() 方法与 findIndex() 方法在数组中查找满足特定条件的元素时会非常有用,其中前者会返回满足条件的第一个元素,而后者会返回该元素的索引位置;
  3. 类型化数组并不是严格的数组,它们并没有继承 Array 对象,但它们的外观和行为都与数组有许多相似点。类型化数组包含的数据类型是八种数值数据类型之一,基于数组缓冲区对象建立,用于表示按位存储的一个数值或一系列数值。类型化数组能够明显提升按位运算的性能,因为它不像 JS 的常规数值类型那样需要频繁进行格式转换。