第6章 集合引用类型
第6章 集合引用类型
Object 类型
通过对象字面量创建的对象实际上未调用Object构造函数。
Array 类型
使用构造函数创建数组:
let names = new Array("Ciri");
console.log(names.length); //1
let arr = new Array(20);
console.log(arr.length); //20
同对象字面量创建一样,数组字面量创建也不调用Array构造函数。
Array构造函数的两个静态方法:Array.from()方法用于转换类数组结构为数组对象,其第二个可选参数为映射函数,第三个可选参数指定this值(不适用箭头函数)。Array.of()将参数转换为数组,相当于Array.prototype.slice.call(arguments)。
const map = new Map().set(1,2).set(3,4);
console.log(map);//{1 => 2, 3 => 4}
console.log(typeof map);//object
const set = new Set().add(1).add(2).add(3).add(4);
console.log(set);//{1, 2, 3, 4}
console.log(typeof set);//object
console.log(Array.from(map));// [[1,2],[3,4]]
console.log(Array.from(set));//[1,2,3,4]
const a = [1,2,3,4,5,6];
const a2 = Array.from(a,x=>x**2);
console.log(a2);//[1,4,9,16,25,36]
const a3 = Array.from(a,function(x){return x**this.y},{y:3});
console.log(a3);//[1,8,27,64,125,216]
const iter = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
yield 4;
}
};
console.log(Array.from(iter)); // [1, 2, 3, 4]
ES6会把数组里的‘洞’当作undefined:
const a = [1,2,,,6];
console.log(Array.from(a));//[1, 2, undefined, undefined, 6]
console.log(a.map(()=>666));//[666, 666, empty × 2, 666] map跳过为‘洞’的项。
Array.isArray用来检测给定的值是不是数组,而不管是不是在当前全局执行环境中创建的。而instanceof不可以。
迭代器方法,keys()、values()和entries():
const a = ['Ciri','Geralt','Triss','Yennefer'];
console.log(Array.from(a.keys()));//[0, 1, 2, 3]
console.log(Array.from(a.values()));
//["Ciri", "Geralt", "Triss", "Yennefer"]
console.log(Array.from(a.entries()));
//[[0,'Ciri'],[1,'Geralt'],[2,'Triss'],[3,'Yennefer']]
for(const [index,element] of a.entries()){
console.log(index);
console.log(element);
}
// 0
// Ciri
// 1
// Geralt
// 2
// Triss
// 3
// Yennefer
ES6的两个新方法fill()和copyWithin(),可批量填充和复制数组,操作的索引范围包含start不包含end。不修改数组大小。
const z = [0,0,0,0,0,0,0,0,0,0];
f = z.fill(5);
console.log(f);//[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
console.log(z);//[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
z.fill(0);
z.fill(6,5);
console.log(z);//[0, 0, 0, 0, 0, 6, 6, 6, 6, 6]
z.fill(0);
z.fill(5,1,3);
console.log(z);//[0,5,5,0,0,0,0,0,0,0]
z.fill(0);
z.fill(8,-9,-3);
console.log(z);//[0,8,8,8,8,8,8,0,0,0]
z.fill(0);
z.fill(8,z.length-9,z.length-3);//同上
console.log(z);//[0,8,8,8,8,8,8,0,0,0]
z.fill(0);
z.fill(3,7,14);
console.log(z);//[0,0,0,0,0,0,0,3,3,3] 超出被忽略
let ints;
let reset = () => ints = [0,1,2,3,4,5,6,7,8,9];
reset();
let cpy = ints.copyWithin(3);
//从索引3处开始替换成原数组从start到end的元素
console.log(ints);//[0, 1, 2, 0, 1, 2, 3, 4, 5, 6]
console.log(cpy); //[0, 1, 2, 0, 1, 2, 3, 4, 5, 6]
reset();
console.log(ints.copyWithin(0,3));
//[3,4,5,6,7,8,9,7,8,9]从索引0处开始替换成原数组从索引3到end的元素。
reset();
console.log(ints.copyWithin(1,4,6));
//[0,4,5,3,4,5,6,7,8,9] 从索引1处开始替换成原数组从索引4到索引6的元素。
栈
后进先出,push()接受任意数量的参数,并将其添加到数组结尾,返回新的数组长度。pop()移除数组尾项并返回,递减length属性。二者组合模拟栈:
let stack = new Array();
console.log(stack.push(1, 2, 3)); //3
stack.push(4);
console.log(stack); //[1,2,3,4]
console.log(stack.pop()); //4
队列
先进先出,shift()移除第一项并返回,递减length属性。 组合push()模拟队列:
let queue = new Array();
console.log(queue.push(0,1,2));//3
console.log(queue.push(3));//4
console.log(queue);//[0, 1, 2, 3]
console.log(queue.shift());//0
反向队列
unshift()添加任意项到数组开头,并返回新的数组长度。组合pop()模拟反向队列:
let r = new Array();
console.log(r.unshift(0,1,2,3));//4
console.log(r.unshift(4));//5
console.log(r.unshift(5));//6
console.log(r);//[5,4,0,1,2,3]
console.log(r.pop());//3
排序方法:
let c = [3, 4, 3, 6, 7, 555, 666, 10, 100];
console.log(c.sort()); //[10, 100, 3, 3, 4, 555, 6, 666, 7]
function compare(a, b) {
return a - b
};
//a,b为数字类型或其valueOf()为数字类型。
console.log(c.sort(compare)); //[3, 3, 4, 6, 7, 10, 100, 555, 666]
console.log(c); // [3, 3, 4, 6, 7, 10, 100, 555, 666]
reverse()和sort()方法均返回使用该方法的数组的引用。
是否可扁平化[Symbol.isConcatSpreadable]:
let a = [1, 2, 3];
let b = ['a', 'b', 'c'];
console.log(a.concat('666', b)); //[1,2,3,"666","a","b","c"]
b[Symbol.isConcatSpreadable] = false;
console.log(a.concat(b)); //[1, 2, 3, Array(3)]
b[Symbol.isConcatSpreadable] = true;
let c = {
[Symbol.isConcatSpreadable]: true,
length: 2,
0: 'a',
1: 'b'
}
console.log(a.concat(c)); //[1, 2, 3, "a", "b"]
操纵方法
concat()方法先复制调用该方法的数组到新数组,然后添加该方法的参数到新数组后面,最后返回新数组,如果没有参数,则简单的复制数组并返回之:
let arr = ['Ciri', 'Geralt', 'Yennefer'];
console.log(arr.concat('Triss')); // ["Ciri", "Geralt", "Yennefer", "Triss"]
console.log(arr); //["Ciri", "Geralt", "Yennefer"]
console.log(arr.concat()); //["Ciri", "Geralt", "Yennefer"]
Slice() 浅复制 (如果数组中的元素是对象或者数组,它们只会被复制引用)原数组的一项或多项到新数组并返回。该方法接受一个或两个参数:开始处索引(包含),结束处索引(不包含)。如果只有一个参数,则返回原数组索引到最后的项。如下所示:
let names = ['Ciri', 'Geralt', 'Yennefer', 'Triss'];
console.log(names.slice(1)); //['Geralt', 'Yennefer', 'Triss']
console.log(names); // ["Ciri", "Geralt", "Yennefer", "Triss"]
console.log(names.slice(1, 3)); //["Geralt", "Yennefer"]
Splice()方法返回删除的元素。参数依次代表:开始删除或替换的索引(包含),删除的个数,替换的元素。
let a = [1,2,3,4,5,6,7,8,9,0];
console.log(a.splice(0,2));//[1, 2]
console.log(a);//[3, 4, 5, 6, 7, 8, 9, 0]
console.log(a.splice(2,0,'a','b','c'));//[]删除0个
console.log(a);//[3, 4, "a", "b", "c", 5, 6, 7, 8, 9, 0]
console.log(a.splice(2,3,'666'));//["a", "b", "c"]
console.log(a);//[3, 4, "666", 5, 6, 7, 8, 9, 0]
搜索和定位方法
indexOf()、lastIndexOf()、includes()方法(ES7)都接受两个参数:要查询的项和一个可选的参数——开始查询的索引:
let nums = [1, 2, 3, 4, 5, 6, 7, 8, 6, 6];
console.log(nums.indexOf(6)); //5
console.log(nums.lastIndexOf(6)); //9
console.log(nums.includes(6)); //true
console.log(nums.includes(3, 2)); //true 索引2处为三
console.log(nums.includes(2, 3)); //false
console.log(nums.includes(6, 1)); //true
ECMAScript的三个严格相等(===)查询方法:所有版本的indexOf()和lastIndexOf(),ES7的includes()。
let a = {name:'Ciri'};
let b = [{name:'Ciri'}];
let c = [a];
let d = [1,2,3,'4','5'];
console.log(b.indexOf(a));//-1 不同的对象
console.log(c.indexOf(a));//0
console.log(b.includes(a));//false
console.log(c.includes(a));//true
console.log(a === b[0]);//false
console.log(d.indexOf(4));//-1
console.log(d.lastIndexOf(5));//-1
断言搜索
ECMAScript允许定义predicate函数,这将在每个索引处调用。函数返回值将决定该索引处的元素是否匹配。
断言方法形如predicate(element, index, array) ,element是数组中正在被检测的元素,index是元素在数组中的索引,array为数组实例。若匹配则返回true。
find()和findIndex()方法从索引最低处开始搜索,find()返回第一个匹配的元素,findIndex返回第一个匹配的元素的索引。两个方法均接受第二个可选参数,以允许在断言内指定this值。
let a = [{name:'Ciri',age:18},{name:'Geralt',age:108}];
console.log(a.find((e)=>e.age<100));//{"age":18,"name":"Ciri"}
console.log(a.findIndex((e,i)=>e.age>100));//1
迭代的方法
以下五个方法都接受两个参数:要在每一项上运行的函数,和运行此函数的对象(影响this值):
every() 在数组中的每个项上运行给定的函数,如果函数对每项都返回true,则返回true。
some() 在数组中的每个项上运行给定的函数,如果函数对某一项返回true,则返回true。
foreach() 在数组中的每个项上运行给定的函数,无返回值。
map() 在数组中的每个项上运行给定的函数,返回每次函数调用后的结果的数组。
filter() 在数组中的每个项上运行给定的函数,返回函数调用结果为true的项的数组。
let a = [1,2,3,4,5,6,7,8,9,0];
console.log(a.every((e,i,a) => e%2 == 0));//false
console.log(a.some((e,i,a) => e%2 == 0));//true
console.log(a.map((e,i,a) => e + i**2));// [1,3,7,13,21,31,43,57,73,81]
console.log(a.filter((e,i,a) => e % 3 == 0));//[3,6,9,0]
reduce()和reduceRight()迭代数组所有的项,返回一个值。两个方法都接受两个参数:每一项都要调用的函数,和一个可选的初始值(返回值相关)。传递给这两个方法的函数的参数分别为:前一个值、当前值、项的索引、数组对象。函数返回的值将自动传递给调用下一项时函数的第一个参数。第一次迭代时发生在数组第二项上,所以第一个参数是数组的第一项,第二个参数是第二项。如下所示:
const a = [1, 2, 3, 4, 5];
const reducer = (prev, cur, index, array) => prev - cur;
console.log(a.reduce(reducer)); //1-2-3-4-5 = -13
console.log(a.reduce(reducer, 100)); //100-1-2-3-4-5 = 85
console.log(a.reduceRight(reducer)); //5-4-3-2-1 = -5
console.log(a.reduceRight(reducer, 100)); //100-5-4-3-2-1 = 85
定型数组
JavaScript定型数组是一种类似数组的对象,并提供了一种用于访问原始二进制数据的机制。Array存储的对象能动态增多和减少,并且可以存储任何JavaScript值。JavaScript引擎会做一些内部优化,以便对数组的操作可以更快。然而,随着Web应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑,访问WebSockets的原始数据等。
在定型数组上调用Array.isArray()会返回false。此外,并不是所有可用于正常数组的方法都能被定型数组所支持(如push和pop)。
WebGL
最终的JavaScript API基于OpenGL for Embedded Systems(OpenGL ES)2.0规范,该规范是OpenGL的子集,专门研究2D和3D计算机图形。新的API名为Web Graphics Library(WebGL),于 2011 年 3 月发布了1.0版。使用它,开发人员能够编写图形密集型应用程序代码,而这些代码可以由任何兼容WebGL的Web浏览器原生解释。
在WebGL的初始版本中,JavaScript数组和原生数组之间的不匹配导致性能问题。图形驱动程序API通常不希望将数字以JavaScript的默认双浮点格式传递给它们。此外,图形驱动程序API希望以二进制格式将数字数组传递给它们,这当然不像内存中的JavaScript数组格式那样。因此,每次在WebGL和JavaScript运行时之间传递数组时,WebGL绑定都将执行昂贵的操作,即在目标环境中分配新数组,以其当前格式迭代该数组,并在新数组中将其转换为合适的格式。
使用ArrayBuffer
Float32Array实际上是一种“view”类型,它允许JavaScript运行时访问分配的内存块ArrayBuffer。ArrayBuffer是所有定型数组和视图引用的基本单位。
ArrayBuffer是一个普通的JavaScript构造函数,可用于在内存中分配特定数量的字节。
创建ArrayBuffer后就无法调整大小。但是,可以使用slice()将现有ArrayBuffer的全部或部分复制到新实例中:
const buf = new ArrayBuffer(16);
console.log(buf.byteLength);//16
console.log(buf.slice(4,12).byteLength);
//8 slice()返回索引4(包含)~12(不包含)处字节。
ArrayBuffer在某些方面类似于C++ malloc(),但有几个值得注意的例外:
当malloc()分配失败时,它将返回一个空指针。如果ArrayBuffer分配失败,则会引发错误。
malloc()调用可以使用虚拟内存,因此分配的最大大小仅受可寻址系统内存的限制。ArrayBuffer分配
不能超过Number.MAX_SAFE_INTEGER(2 ^ 53)字节。
成功的malloc()调用不执行实际地址的初始化。声明ArrayBuffer会将所有位初始化为 0 。
在调用free()或程序退出之前,系统无法使用malloc()分配的堆内存。通过声明ArrayBuffer分配的
堆内存可被垃圾回收——无需手动进行内存管理。
只能通过引用ArrayBuffer实例来读取或写入ArrayBuffer的内容。要在内部读取或写入数据,必须在视图中进行。视图有不同类型,但是它们都引用存储在ArrayBuffer中的二进制数据。
DataViews
允许读取和写入ArrayBuffer的一种视图是DataView。此视图是为文件I/O和网络I/O设计的。该API允许在处理缓冲数据时进行高度控制,但是与其他的视图类型相比,它降低了性能开销。 DataView不擅用有关缓冲区的任何内容,并且不可迭代。
必须创建一个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则view从缓冲区前端开始,byteLength=8限制view到前八个字节:
const firstHalfDataView = new DataView(buf, 0, 8);
console.log(firstHalfDataView.byteOffset); // 0
console.log(firstHalfDataView.byteLength); // 8
console.log(firstHalfDataView.buffer === buf); // true
// 不指定的话 DataView将使用剩下的缓冲区,byteOffset=8则view从缓冲区的第九个字节处开始,byteLength默认为剩下的缓冲区字节长度:
const secondHalfDataView = new DataView(buf, 8);
console.log(secondHalfDataView.byteOffset); // 8
console.log(secondHalfDataView.byteLength); // 8
console.log(secondHalfDataView.buffer === buf); // true
要通过DataView读取和写入缓冲区,将需要使用以下组件:
读取或写入的字节偏移量,可以将其视为DataView中的一种“地址”。
DataView应使用ElementType在JavaScript运行时中的Number类型和缓冲区中的二进制格式之间进行转
换。
内存中值的字节序(endianness)。默认为大端(big-endian)。
ElementType
DataView不关心缓冲区中的数据类型,它公开的API会强制你在读取或写入时指定ElementType,并且DataView将负责执行转换以读取或写入。
ES6支持 8 种ElementTypes:
ElementTypes | 字节 | 描述 | 等同于c语言中的 | 值范围 |
---|---|---|---|---|
Int8 | 1 | 8位整数 | signed char | -128 ~ 127 |
Uint8 | 1 | 8位无符号整数 | unsigned char | 0 ~ 255 |
Int16 | 2 | 16位整数 | short | -32768 ~ 32767 |
Uint16 | 2 | 16位无符号整数 | unsigned short | 0 ~ 65535 |
Int32 | 4 | 32位整数 | int | -2,147,483,648 ~ 2,147,483,647 |
Uint32 | 4 | 32位无符号整数 | unsigned int | 0 ~ 4,294,967,295 |
Float32 | 4 | 32位IEEE-754浮点数 | float | -3.4E+38 ~ +3.4E+38 |
Float64 | 8 | 64位IEEE-754浮点数 | double | -1.7E+308 ~ +1.7E+308 |
DataView公开每种类型的get和set方法,这些方法使用byteOffset寻址到缓冲区以读取和写入值。类型可以互换使用,如下所示:
// 分配2字节的内存并声明一个DataView
const buf = new ArrayBuffer(2);
const view = new DataView(buf);
//演示表示整个缓冲区的确都是0,检查第一个和第二个字节
console.log(view.getInt8(0)); // 0
console.log(view.getInt8(1)); // 0
// 检查整个缓冲区
console.log(view.getInt16(0)); // 0
// 设置整个缓冲区为1,255在二进制中是11111111 (2^8 – 1)
// 语法:dataview.setUint8(byteOffset, value)
view.setUint8(0, 255);
console.log(view.getUint8(0)); //255
// DataView将转换值为合适的类型,255十六进制表示为0xFF
view.setUint8(1, 0xFF);
console.log(view.getUint8(1)); //255
//缓冲区现在都是1了,当作为16位整形读取时为-1,无符号16位整型为65535
console.log(view.getInt16(0)); // -1
console.log(view.getUint16(0)); //65535
大端和小端
上一个示例中的缓冲区字节是相同的,以避免出现字节顺序问题。 “字节序”是指计算系统维护的字节顺序的约定。就DataView而言,仅支持两种约定:big-endian和little-endian。Big-endian,也称为“network byte order”,是指 最高有效字节保留在第一个字节中,最低有效字节保留在最后一个字节中 。little-endian表示最低有效字节保留在第一个字节中,而最高有效字节保留在最后一个字节中。
执行JavaScript运行时的系统的原生字节序将决定其读取和写入字节的方式,但是DataView不遵守该约定。 DataView是内存段的无偏接口,并且将遵循指定的任何字节序。所有DataView API方法默认为big-endian约定,但是接受一个可选的final布尔参数,将该参数设置为true来启用little-endian。如下所示:
// 分配2字节的内存并声明一个DataView
const buf = new ArrayBuffer(2);
const view = new DataView(buf);
// 填充缓冲区让第一个位和最后一个位为1
view.setUint8(0, 0x80); // 设置最左边的位为1
console.log(0x80.toString(2)); //10000000
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
小案例
DataView只有在有足够缓冲空间时才能完成读写,否则将抛一个RangeError:
const buf = new ArrayBuffer(6);
const view = new DataView(buf);
// Int32是4字节,一部分会超出缓冲区:
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
定型数组
定型数组是ArrayBuffer视图的另一种形式 。其在概念上与DataView相似,并遵守系统的原生字节序。定型数组提供了更广泛的API和更好的性能。定型数组旨在与原生库(例如WebGL)有效地交换二进制数据。因为定型数组的二进制表示形式对于原生操作系统而言是易于消化的格式,所以JavaScript引擎能够在定型数组上极大地优化算法,按位运算(bitwise)和其他常见操作,因此,它们使用起来非常快。
可以创建定型数组读取现有缓冲区,使用自己的缓冲区进行初始化,使用可迭代对象填充或从任何类型的现有定型数组来填充。
这可通过使用.from()和.of()创建:
// 创建一个12字节的缓冲区
const buf = new ArrayBuffer(12);
// 创建一个引用该缓冲区的Int32Array
const ints = new Int32Array(buf);
//Int32Array的每个元素需要4字节,所以长度为3
console.log(ints.length); // 3
// 创建一个长度为6的Int32Array
const ints2 = new Int32Array(6);
// 每个数字使用4字节, 所以ArrayBuffer是24字节
console.log(ints2.length); // 6
// 象DataView一样, 定型数组有与它们关联的缓冲区的引用
console.log(ints2.buffer.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 = Int16Array.from([3, 5, 7, 9]);
console.log(ints5.length); // 4
console.log(ints5.buffer.byteLength); // 8
console.log(ints5[2]); // 7
// 从参数创建一个Float32Array
const floats = Float32Array.of(3.14, 2.718, 1.618);
console.log(floats.length); // 3
console.log(floats.buffer.byteLength); // 12
console.log(floats[2]); // 1.6180000305175781
定型数组的行为
在大多数情况下,定型数组的行为都与常规数组类似。定型数组支持以下运算符,方法和属性:
[]
copyWithin()
entries()
every()
fill()
filter()
find()
findIndex()
forEach()
indexOf()
join()
keys()
lastIndexOf()
length
map()
reduce()
reduceRight()
reverse()
slice()
some()
sort()
toLocalString()
toString()
values()
返回新数组的方法用在这里将返回新定型数组:
const ints = new Int16Array([1, 2, 3]);
const doubleints = ints.map(x => 2*x);
console.log(doubleints instanceof Int16Array); // true
定型数组定义了Symbol.iterator,这意味着还可以使用for..of循环和延展运算符:
const ints = new Int16Array([1, 2, 3]);
for (const int of ints) {
console.log(int);
}
// 1
// 2
// 3
console.log(Math.max(...ints)); // 3
合并 复制 和改变定型数组
定型数组仍将数组缓冲区用作其存储,并且数组缓冲区无法调整大小。因此,类型数组不支持以下方法:
concat()
pop()
push()
shift()
splice()
unshift()
但是,定型数组提供了两个新方法,可快速将值复制到数组中和从数组中复制出:set()和subarray()。
set()将提供的数组或定型数组中的值复制到定型数组中的指定索引处:
// 创建一个长度为8的Int16Array数组
const container = new Int16Array(8);
// 复制到定型数组中的前四个位置,偏移量默认为索引0
container.set(Int8Array.of(1, 2, 3, 4));
console.log(container); // Int16Array(8)[1,2,3,4,0,0,0,0]
// 复制普通数组到剩下位置,偏移为索引4处
container.set([5, 6, 7, 8], 4);
console.log(container); // Int16Array(8)[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]
定型数组没有原生的合并能力,但是定型数组API中提供了很多工具,这些工具可以手动构建:
//第一个参数是应返回的数组类型,其余参数是要合并的定型数组
function typedArrayConcat(typedArrayConstructor, ...typedArrays) {
// 计算所有数组的元素数
const numElements = typedArrays.reduce((x, y) =>
(x.length || x) + y.length);
// 创建提供的类型的数组,长度为numElements
const resultArray = new typedArrayConstructor(numElements);
// 执行数组元素转移
let currentOffset = 0;
typedArrays.map(x => {
resultArray.set(x, currentOffset);
currentOffset += x.length;
});
return resultArray;
}
const concatArray = typedArrayConcat(Int32Array,
Int8Array.of(1, 2, 3),
Int16Array.of(4, 5, 6),
Float32Array.of(7, 8, 9));
console.log(concatArray); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(concatArray instanceof Int32Array); // true
下溢和上溢
定型数组中值的上溢和下溢不会溢出到其他索引中,但仍需考虑该数组将其各项视为哪种元素类型。定型数组将仅接受数组中每个索引可以保存的相关位,而不管其对实际数值的影响。下面演示了如何处理下溢和上溢:
// 长度为2的整型数组,每个索引占用一个补码位故整型值范围为:-128(-1 * 2^7)到127(2^7 – 1)
// -128转换过程:128为 1000 0000 反码为:0111 1111 补码为反码加1即为-128:1000 0000
const ints = new Int8Array(2);
//长度为2的无符号整型数组,每个索引处的整型值范围为:0到255(2^7 – 1)
const unsignedInts = new Uint8Array(2);
// 上溢的位不会溢出到相邻索引,索引只占用最低有效(least significant)的8位
unsignedInts[1] = 256; // 0x100
console.log(unsignedInts); // [0, 0]
unsignedInts[1] = 511; // 0x1FF
console.log(unsignedInts); // [0, 255]
// 下溢的位会转换为相应的无符号整型,1为:0000 0001 反码:1111 1110 补码1111 1111即为-1 作为无
符号整型即为255
unsignedInts[1] = -1 // 0xFF (truncated to 8 bits)
console.log(unsignedInts); // [0, 255]
ints[1] = 128; // 0x80
console.log(ints); // [0, -128]
// 1二进制表示为 0000 0001 反码:1111 1110 所以-1为1111 1111
ints[1] = 255; // 0xFF
console.log(ints); // [0, -1]
除了八种元素类型之外,还有一个附加的“clamped”数组类型Uint8ClampedArray,可以防止在任一方向上溢出。超过最大值 255 的值将四舍五入为 255 ,低于 0 的值将四舍五入为0:
const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256]);
console.log(clampedInts); // [0, 0, 255, 255]
Uint8ClampedArray完全是HTML5 canvas元素的历史产物。除非确实在做画布类的事情,否则请避免。
Map 类型
ECMAScript 6中新添加了Map,它是一种新的集合类型,它将真正的键/值行为引入了该语言。它提供的功能与Object类型提供的功能有很多重叠,但是在选择使用对象时,应考虑到Object和Map类型之间的细微差别。
基础API
如果希望在初始化Map时填充它,则构造函数可以选择接受一个可迭代的对象,期望它包含键/值对数组。可迭代参数中的每对将以其迭代顺序插入新创建的Map中:
// 使用嵌套数组初始化map
const m1 = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
console.log(m1.size); // 3
// 使用自定义迭代器初始化map
const m2 = new Map({
[Symbol.iterator]: function*() {
yield ["key1", "val1"];
yield ["key2", "val2"];
yield ["key3", "val3"];
}
});
console.log(m2.size); // 3
// Map期待值是键值对,而不管是否提供
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(m.has("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()方法返回Map实例,因此可以将多个set操作链接在一起,包括在初始声明中:
const m = new Map().set("key1", "val1");
m.set("key2", "val2")
.set("key3", "val3");
console.log(m.size); // 3
与只能使用数字或字符串作为键的Object不同,Map可以使用任何JavaScript数据类型作为键。它使用“SameValueZero”比较操作(在ECMAScript规范中定义,但在实际语言中不可用),并且与使用严格相等检查键相等。与对象一样,对值中包含的内容没有限制。如下所示:
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实例保持键/值对的插入的顺序,并按照插入顺序执行迭代操作。
Map实例可以提供一个Iterator,该Iterator以插入顺序包含[key,value]形式的数组对。可以使用entries()或Symbols.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()是默认的迭代器,所以可以使用延展操作符将Map简洁地转换为数组:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
console.log([...m]); // [[key1,val1],[key2,val2],[key3,val3]
若要使用回调而不是迭代器,forEach(callback,opt_thisArg)可为每个键/值对调用回调。它可以选择接受第二个参数,它将在每次回调调用中覆写this值:
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()返回一个迭代器,该迭代器按插入顺序包含Map中的所有键或所有值:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
for (let key of m.keys()) {
console.log(key);
}
// key1
// key2
// key3
for (let key of m.values()) {
console.log(key);
}
// value1
// value2
// value3
迭代器内部公开的键和值是可变的,但Map内的引用不能更改。但是,这并不限制更改键或值对象内的属性。这样做不会改变它们相对于Map实例的身份:
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"]
]);
// 键对象属性改变了,但对象在map种仍然引用相同的值
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
对于大多数Web开发而言,在Map或常规Object之间进行选择只是个人偏爱,而在其他地方却没有多大影响。但是,对于关心内存和性能的开发人员来说,Object和Map之间可能存在明显的差异。
内存简况
Object和Map的引擎级实现因浏览器差异而有所不同,但是存储单个键/值对所需的内存量与键的数量成线性比例。键/值对的批量添加或删除也受引擎如何实现该类型的内存分配的支配。结果可能因浏览器而异,但是给定固定的内存量,Map将比Object多存储大约 50 %的键/值对。
插入性能
将新的键/值对插入到Object与Map中是大致可比较的操作,但是在所有浏览器引擎中,插入Map通常会稍快一些。对于这两种类型,插入的速度都不随Object或Map中的键/值对的数量线性变化。如果代码执行大量插入操作,则Map实例可提供卓越的性能。
查询性能
与插入不同,在Object和Map中查找键/值对在规模上是大致可比较的操作,但是在某些情况下,较少数量的键/值对有利于Object实例。在像数组一样使用Object实例的情况下(例如,连续的整数属性),浏览器引擎可以执行优化操作,例如在内存中进行更有效的布局——Map永远不可能做到这一点。对于这两种类型,查找速度都不随Object或地图中键/值对的数量线性变化。如果代码在查找操作上比较繁琐,则在某些情况下,使用Object可能会更有利。
删除性能
众所周知,对Object属性执行删除操作的性能令人震惊,在许多浏览器引擎中,情况仍然如此。伪删除Object属性的解决方法包括为属性分配undefined或null值,但是在许多情况下,这是令人讨厌或不合适的折衷方案。在大多数浏览器引擎中,Map的delete()操作比插入和查找要快。如果代码需大量删除操作,则Map类型将是首选。
WEAKMAP 类型
ES6中新增了WeakMap,它是一种新的集合类型,它将增强的键/值行为引入了该语言。 WeakMap类型是Map类型的表亲,其API是Map的严格子集。 “弱”名称描述了JavaScript的垃圾收集器如何处理WeakMap中的键。
基础API
weakMap的键只能是Object类型或继承自Object的类型,设置键为其他类型将抛出异常。值类型没有限制。
如果希望在初始化WeakMap时填充它,则构造函数可以选择接受一个可迭代的对象,期望它包含有效的键/值对数组。可迭代参数中的每对将以其迭代顺序插入新创建的WeakMap中:
const key1 = {
id: 1
},
key2 = {
id: 2
},
key3 = {
id: 3
};
// 使用嵌套数组初始化WeakMap
const wm1 = new WeakMap([
[key1, "val1"],
[key2, "val2"],
[key3, "val3"]
]);
console.log(wm1.get(key1)); // val2
console.log(wm1.get(key2)); // val2
console.log(wm1.get(key3)); // val3
// 要么全部初始化要么一个也不,单个不符合规则的键将抛出异常并中断初始化
const wm2 = new WeakMap([
[key1, "val1"],
["BADKEY", "val2"], // TypeError: Invalid value used as WeakMap key.
[key3, "val3"]
]);
const stringKey = new String("key1");
const wm3 = new WeakMap([
stringKey, "val1" //Invalid value used as weak map key.
]);
console.log(wm3.get(stringKey)); // "val1"
键值对可在weakMap初始化后可使用set()添加、使用get()和has()查询、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
set()方法返回WeakMap实例,因此可以将多个set操作链接在一起,包括在初始化声明中:
const key1 = {id: 1},
key2 = {id: 2},
key3 = {id: 3};
const wm = new WeakMap().set(key1, "val1");
wm.set(key2, "val2")
.set(key3, "val3");
console.log(wm.get(key1)); // val1
console.log(wm.get(key2)); // val2
console.log(wm.get(key3)); // val3
弱键
“弱”表示源于以下事实:WeakMap中的键是“弱持有”的,这意味着它们不被视为会阻止垃圾收集的正式引用。 WeakMap的一个重要区别是,值引用并不弱。只要键存在,键/值对将保留在Map中,并作为对该值的引用——从而防止将其垃圾回收。如下所示:
const wm = new WeakMap();
wm.set({}, "val");
在set()内部,将初始化一个新对象并将其用作键。因为没有其他对该对象的引用,所以在此行完成执行后,该对象键将被垃圾回收释放。发生这种情况时,键/值对将从WeakMap中消失,并且为空。在此示例中,因为没有其他对该值的引用,所以此键/值销毁也将意味着该值符合垃圾回收的条件。如下所示:
const wm = new WeakMap();
const container = {
key: {}
};
wm.set(container.key, "val");
function removeReference() {
container.key = null;
}
此处,container对象维护对WeakMap中键的引用,因此该对象不符合垃圾回收的条件。但是,一旦调用removeReference(),对键对象的最后一个强引用将被销毁,并且垃圾回收将最终清除键/值对。
不可迭代的键
由于WeakMap中的键/值对可以随时销毁,因此提供迭代键/值对的能力是没有意义的。这还排除了使用clear()一次销毁所有键/值对的能力,而这不是WeakMap API的一部分。因为不可能进行迭代,所以除非有对键对象的引用,否则无法从WeakMap实例获取键。即使代码可以访问WeakMap实例,也无法检查其内容。
WeakMap实例将键限制在对象内的原因是为了保留以下约定:只能从对键对象的引用获取WeakMap中的值。
用途
WeakMap实例与现有的JavaScript工具有着显著的不同,而如何使用它们可能并不显而易见。对此没有唯一的答案,但是已经出现了许多策略。
私有变量
WeakMap实例提供了一种在JavaScript中实现真正的私有变量的全新方法。原理很简单:私有变量将存储在WeakMap中,对象实例作为键,私有成员的字典作为值。实现如下:
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() {
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即可获取“私有”变量。为了防止这种情况,可以将WeakMap包裹在一个闭包中,以完全从外部代码隐藏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);
console.log(user.getId()); // 123
user.setId(456);
console.log(user.getId()); // 456
因此,如果没有用于插入的键,则无法检索WeakMap中的值。虽然,这样可以防止上述访问模式,但是在某些方面,它已将代码推到了ES6之前的闭包私有变量模式。
DOM节点元数据
由于WeakMap实例不会干扰垃圾回收,因此它们是免费清理关联元数据的绝佳工具。考虑以下示例,该示例使用常规Map:
const m = new Map();
const loginButton = document.querySelector('#login');
// 使用节点关联一些元数据
m.set(loginButton, {
disabled: true
});
假设执行此代码后,JavaScript会更改页面,并且从DOM树中删除登录按钮。因为在Map内部存在引用,所以DOM节点将永久保留在内存中,直到从Map中显式删除它,或者直到Map被破坏为止。
如果改为使用WeakMap(如下面的代码所示),则从DOM中删除该节点将使垃圾收集器立即释放已分配的内存(假定对该对象没有其他持久的引用):
const wm = new WeakMap();
const loginButton = document.querySelector('#login');
// 使用节点关联一些元数据
wm.set(loginButton, {
disabled: true
});
Set 类型
ES6中新增了Set,它是一种新的集合类型,它将集合行为引入了Javascript。由于许多API和行为是共享的,因此Set在许多方面的行为都像Map。
基础API
Set构造函数可以选择接受一个可迭代对象,该对象包含要添加到新创建的Set实例中的元素:
// 使用数组初始化set
const s1 = new Set(["val1", "val2", "val3]);
console.log(s1.size); // 3
// 使用自定义迭代器初始化set
const s2 = new Set({
[Symbol.iterator]: function*() {
yield "val1";
yield "val2";
yield "val3";
}
});
console.log(s2.size); // 3
值可在Set初始化后可使用add()添加,使用has()查询,使用size属性计数,使用delete()和clear()删除:
const s = new Set();
console.log(s.has("Ciri")); // false
console.log(s.size); // 0
s.add("Ciri")
.add("Geralt");
console.log(s.has("Ciri")); // true
console.log(s.size); // 2
s.delete("Ciri");
console.log(s.has("Ciri")); // false
console.log(s.has("Geralt")); // true
console.log(s.size); // 1
s.clear(); // 摧毁此Set实例中所有值
console.log(s.has("Ciri")); // false
console.log(s.has("Geralt")); // false
console.log(s.size); // 0
add()方法返回Set实例,因此可以将多个操作链接在一起,包括在初始化声明中:
const s = new Set().add("val1").add("val2").add("val3");
console.log(s.size); // 3
与Map一样,Set可以包含任何JavaScript数据类型作为值。它使用“ SameValueZero”比较操作(在ECMAScript规范内定义,但在实际语言中不可用),并且与使用严格对象相等(===)检查值的匹配情况。对值中包含的内容没有限制:
const s = new Set();
const functionVal = function() {};
const symbolVal = Symbol();
const objectVal = new Object();
s.add(functionVal);
s.add(symbolVal);
s.add(objectVal);
console.log(s.has(functionVal)); // true
console.log(s.has(symbolVal)); // true
console.log(s.has(objectVal)); // true
// SameValueZero检查意味着分开的实例不会起冲突
console.log(s.has(function() {})); // false
用于Set中值的对象或其他集合类型的属性或内容发生改变时:
const s = new Set();
const objVal = {},
arrVal = [];
s.add(objVal).add(arrVal);
objVal.bar = "bar";
arrVal.push("bar");
console.log(s.has(objVal)); // true
console.log(s.has(arrVal)); // true
add()和delete()操作是幂等的,delete()返回布尔值指示删除的值是否在Set中:
const s = new Set();
s.add('foo');
console.log(s.size); // 1
s.add('foo');
console.log(s.size); // 1
// 值在set中
console.log(s.delete('foo')); // true
// 值不在set中
console.log(s.delete('foo')); // false
顺序和迭代
Set保持值插入的顺序,并按照插入顺序执行迭代操作。
Set实例可以提供一个Iterator,该Iterator以插入顺序包含set的内容。可以使用values()或Symbols.iterator属性来获取迭代器:
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()是默认的迭代器,所以可以使用延展运算符将集合简洁地转换为数组:
const s = new Set(["val1", "val2", "val3"]);
console.log([...s]); // [val1,val2,val3]
entries()返回一个迭代器,该迭代器包含一个包含两个元素的数组,该数组包含Set中所有值的插入顺序的重复项:
const s = new Set(["val1", "val2", "val3"]);
for (let pair of s.entries()) {
console.log(pair);
}
// ["val1", "val1"]
// ["val2", "val2"]
// ["val2", "val2"]
若使用回调而不是迭代器的话,forEach(callback,opt_thisArg)将为每个值调用回调。它可以选择接受第二个参数,该参数将在每次回调调用中覆写此参数的this值:
const s = new Set(["val1", "val2", "val3"]);
s.forEach((val, dupVal) => console.log(`${val} -> ${dupVal}`));
// val1 -> val1
// val2 -> val2
// val3 -> val3
更改Set中值的属性不会更改Set实例的值身份:
const s1 = new Set(["val1"]);
// 基本字符串作为值是不会改变的
for (let value of s1.values()) {
value = "newVal";
console.log(value); // newVal
console.log(s1.has("val1")); // true
}
const valObj = {
id: 1
};
const s2 = new Set([valObj]);
// 值对象属性改变了,但对象仍在set中
for (let value of s2.values()) {
value.id = "newVal";
console.log(value); // {id: "newVal"}
console.log(s2.has(valObj)); // true
}
console.log(valObj); // {id: "newKey"}
定义正式的Set操作
在许多方面,Set感觉就像重排的Map API。其API仅支持自引用操作。许多开发人员会喜欢使用Set操作。
这需要手动实现,并且可以采用Set子集的形式或定义实用程序库。要同时提供这两种功能,可以在子类上实现静态方法,然后在实例方法中使用这些静态方法。实施这些操作时,需要注意以下几点:
一些Set操作是关联的,因此能够实现该方法以便它可以处理任意数量的Set实例。
Set保留插入顺序,并且从这些方法返回的set应该符合该事实。
尽可能有效地使用内存。延展运算符提供了不错的语法,但要避免在集合和数组之间来回切换,以节省
对象初始化成本。
不要更改现有的Set实例。 union(a,b)或a.union(b)应该返回一个带有结果的新Set实例。
class XSet extends Set {
union(...sets) {
return XSet.union(this, ...sets)
}
intersection(...sets) {
return XSet.intersection(this, ...sets);
}
difference(set) {
return XSet.difference(this, set);
}
symmetricDifference(set) {
return XSet.symmetricDifference(this, set);
}
cartesianProduct(set) {
return XSet.cartesianProduct(this, set);
}
powerSet() {
return XSet.powerSet(this);
}
// 返回两个或多个set的并集
static union(a, ...bSets) {
const unionSet = new XSet(a);
for (const b of bSets) {
for (const bValue of b) {
unionSet.add(bValue);
}
}
return unionSet;
}
// 返回两个或多个set的交集
static intersection(a, ...bSets) {
const intersectionSet = new XSet(a);
for (const aValue of intersectionSet) {
for (const b of bSets) {
if (!b.has(aValue)) {
intersectionSet.delete(aValue);
}
}
}
return intersectionSet;
}
// 返回两个set的差
static difference(a, b) {
const differenceSet = new XSet(a);
for (const bValue of b) {
if (a.has(bValue)) {
differenceSet.delete(bValue);
}
}
return differenceSet;
}
// 返回两个set的对称差
static symmetricDifference(a, b) {
// 定义为a∪b - a∩b
return a.union(b).difference(a.intersection(b));
}
// 返回两个set的笛卡尔积(作为数组对),必须返回值为数组的set,因为可能包含相同的一对值
static cartesianProduct(a, b) {
const cartesianProductSet = new XSet();
for (const aValue of a) {
for (const bValue of b) {
cartesianProductSet.add([aValue,cartesianProductSet;
bValue]);
}
// Returns power set of exactly one set.
static powerSet(a) {
const powerSet = new XSet().add(new XSet());
for (const aValue of a) {
for (const set of new XSet(powerSet)) {
powerSet.add(new XSet(set).add(aValue));
}
}
return powerSet;
}
}
WeakSet 类型
ES6中新增了WeakSet,它是一种新的集合类型。 WeakSet类型是Set类型的表亲,其API是Set的严格子集。 “弱”名称描述了JavaScript的垃圾回收器如何处理弱WeakSet中的值。
基础API
WeakSet的值只能是Object类型或继承自Object的类型,设置值为其他类型将抛出异常。
如果希望在初始化WeakSet时填充它,则构造函数可以选择接受一个可迭代的对象,期望它包含有效值。可迭代对象参数中的每个值都将按照迭代的顺序插入到新创建的WeakSet中:
const val1 = {id: 1},
val1 = {id: 2},
val1 = {id: 3},
// 使用嵌套数组初始化WeakSet
const ws1 = new WeakSet([val1, val2, val3]);
console.log(ws1.has(val1)); // true
console.log(ws1.has(val2)); // true
console.log(ws1.has(val3)); // true
// 要么全部初始化要么一个也不,单个不符合规则的键将抛出异常并中断初始化
const ws2 = new WeakSet([val1, "BADVAL", val3]);
// TypeError: Invalid value used in WeakSet
// 可使用基本类型的包装对象作为值
const stringVal = new String("val1");
const ws3 = new WeakSet([stringVal]);
console.log(ws3.has(stringVal)); // true
值可在初始化后使用add()添加,使用has()查询,使用delete()删除:
const ws = new WeakSet();
const val1 = {id: 1},
val1 = {id: 2};
console.log(ws.has(val1)); // false
ws.add(val1)
.add(val2);
console.log(ws.has(val1)); // true
console.log(ws.has(val2)); // true
ws.delete(val1); // 仅删除此值
console.log(ws.has(val1)); // false
console.log(ws.has(val2)); // true
add()方法返回WeakSet实例,因此可以将多个添加操作链接在一起,包括在初始化声明中:
const val1 = {id: 1},
val1 = {id: 2},
val1 = {id: 3},
const ws = new WeakSet().add(val1);
ws.add(val2)
.add(val3);
console.log(ws.has(val1)); // true
console.log(ws.has(val2)); // true
console.log(ws.has(val3)); // true
弱键
WeakSet中的值是“弱保存”的,这意味着它们不被视为正式引用,否则将阻止垃圾回收。如下所示:
const ws = new WeakSet();
ws.add({});
在add()内部,一个新对象被初始化并用作值。因为没有其他对该对象的引用,所以此行执行完后,该对象值将被垃圾回收释放。发生这种情况时,该值将从WeakSet中消失,并且为空。如下所示:
const ws = new WeakSet();
const container = {
val: {}
};
ws.add(container.val);
function removeReference() {
container.val = null;
}
此例中,container对象维护对WeakSet实例中的值的引用,因此该对象不符合垃圾回收的条件。但是,一旦调用removeReference(),对值对象的最后一个强引用将被销毁,垃圾回收最终将清除该值。
不可迭代的值
由于WeakSet中的值可以随时销毁,因此提供遍历这些值的能力是没有意义的。这也排除了使用clear()一次销毁所有值的能力,这不是WeakSet API的一部分。因为不可能进行迭代,所以除非有对value对象的引用,否则也无法从WeakSet实例中获取值。
即使代码可以访问WeakSet实例,也无法检查其内容。
用途
与WeakMap实例相比,WeakSet实例的用途受到更多限制,但对于标记对象仍然很有价值。如下所示:
const disabledElements = new Set();
const loginButton = document.querySelector('#login');
// 通过将其添加到相应set来标记此节点为"disabled"
disabledElements.add(loginButton);
在这里,可以通过查看元素是否存在于disableElements中来检查元素是否被禁用,这可以在固定时间内完成。但是,如果将元素从DOM中删除,则该元素在此Set中的存在将阻止垃圾回收重新分配其内存。
为了允许垃圾回收重新分配元素的内存,可以使用WeakSet:
<button type="button" id="login">登录</button>
const disabledElements = new WeakSet();
const loginButton = document.querySelector('#login');
// 通过将其添加到相应set来标记此节点为"disabled"
disabledElements.add(loginButton);
现在,当从DOM中删除WeakSet中的任何元素时,垃圾收集器在考虑将其用于垃圾收集时将忽略其在WeakSet中的存在。
迭代和延展操作符
ES6介绍了迭代器和延展运算符,它们在集合引用类型的环境中特别有用。这些新工具可轻松实现互操作性,克隆和修改集合类型。
四种原生集合引用类型定义了默认的迭代器:
Array
所有的Typed Array
Map
Set
这意味着它们都支持有序迭代,并且可以将其传递给for..of循环:
let iterableThings = [
Array.of(1, 2),
typedArr = Int16Array.of(3, 4),
new Map([
[5, 6],
[7, 8]
]),
new Set([9, 10])
];
for (const iterableThing of iterableThings) {
for (const x of iterableThing) {
console.log(x);
}
}
// 1
// 2
// 3
// 4
// [5, 6]
// [7, 8]
// 9
// 10
这也意味着这些类型都与延展操作符兼容。延展运算符在对可迭代对象执行浅复制时特别有用。可以使用简洁的语法轻松克隆整个对象:
let arr1 = [1, 2, 3];
let arr2 = [...arr1];
console.log(arr1); // [1, 2, 3]
console.log(arr2); // [1, 2, 3]
console.log(arr1 === arr2); // false
期望可迭代对象的构造函数可以只传递要克隆的可迭代实例:
let map1 = new Map([
[1, 2],
[3, 4]
]);
let map2 = new Map(map1);
console.log(map1); // Map {1 => 2, 3 => 4}
console.log(map2); // Map {1 => 2, 3 => 4}
成为数组的一部分亦可:
let arr1 = [1, 2, 3];
let arr2 = [0, ...arr1, 4, 5];
console.log(arr2); // [0, 1, 2, 3, 4, 5]
浅复制只复制对象引用:
let arr1 = [{}];
let arr2 = [...arr1];
arr1[0].foo = 666;
console.log(arr2[0]); // {foo: 666}
这些集合类型中的每一个都支持多种构造方法,例如Array.of()和Array.from()静态方法。与延展运算符结合使用时,这将使互操作性变得非常容易:
let arr1 = [1, 2, 3];
// 复制数组到定型数组
let typedArr1 = Int16Array.of(...arr1);
let typedArr2 = Int16Array.from(arr1);
console.log(typedArr1); // Int16Array [1, 2, 3]
console.log(typedArr2); // Int16Array [1, 2, 3]
// 复制数组到map
let map = new Map(arr1.map((x) => [x, 'val' + x]));
console.log(map); // Map {1 => ‘val 1’, 2 => ‘val 2’, 3 => ‘val 3’}
// 复制数组到set
let set = new Set(typedArr2);
console.log(set); // Set {1, 2, 3}
// 复制set回数组
let arr2 = [...set];
console.log(arr2); // [1, 2, 3]