JavaScript语言基础(三)集合引用类型

656 阅读23分钟

集合引用类型

Object[常用]

适合存储和在应用程序间交换数据。 显示创建Object实例方式:

(1)使用new操作符和Object构造函数

let person = new Object();
person.name = "Nike";
person.age = 29;

(2)使用对象字面量表示法【开发者更倾向】

是对象定义的简写形式,为了简化包含大量属性的对象的创建。

let person = {
    name: "Nike",
    age: 29
};

属性名可以是字符串或数值,数值属性会自动转换为字符串。 在使用对象字面量表示法定义对象时,并不会实际调用Object构造函数。

通常,点语法是首选的属性存取方式,除非访问属性时必须使用变量。

console.log(person["name"]);	// "Nike"
console.log(person.name);	// "Nike"

使用中括号的主要优势是可以通过变量访问属性,属性名中包含关键字/保留字时,可使用中括号语法。

Array[常用]

ESMAScript数组中每个槽位可以存储任意类型的数据,数组是动态大小的。

创建数组

(1)使用Array构造函数

let colors = new Array(20);
let colors1 = new Array("red","blue","green");

(2)使用数组字面量表示法

是在中括号中包含以逗号分隔的元素列表。

let colors = ["red","blue","green"];

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

ES6新增的用于创建数组的静态方法:form()和of(),

from()

用于将类数组结构转换为数组实例, 第一次参数是一个类数组对象,即任何可迭代的结构,或者一个length属性和可索引元素的结构。

console.log(Array.from("Matt"));	// ["M","a","t","t"]

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

第二个可选的映射函数参数,可直接增强新数组的值。还可以接收第三个可选参数,用于指定映射函数中this的值。

const a1 = [1,2,3,4];
const a2 = Array.from(a1,x => x**2);
console.log(a2);	// [1,4,9,16]

of()

用于将一组参数转换为数组实例。

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

数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位。

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

在实践中要避免使用数组空位,如果确实需要空位,则可以显示地用undefined值代替。

数组索引

要取得或设置数组的值,需要使用中括号并提供相应值的数字索引。

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

let colors = ["red","blue","green"];
colors[colors.length] = "black";	// 添加一种颜色【位置3】
colors[colors.length] = "grey";	// 再添加一种颜色【位置4】

检测数组

在只有一个网页(因而只有一个全局作用域)的情况下,使用instanceof 操作符就可以判断对象是不是数组。

if (value instanceof Array)
{
	// 操作数组
}

if (Array.isArray(value))
{
	// 操作数组
}

Array.isArray()方法

可以确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。

迭代器方法

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

keys()

返回数组索引的迭代器。

values()

返回数组元素的迭代器。

entries()

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

使用ES6的解构可以非常容易地在循环中拆分键/值对:

const a = [ "foo","bar","baz" ];

for(const [idx, element] of a.entries()) {
	alert(idx);
	alert(element);
}
// 0
// foo
// 1
// bar
// 2
// baz

复制和填充方法

ES6新增方法, 都需指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。使用这个方法不会改变数组的大小。

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

批量复制方法copyWithin()

会按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置。开始索引和结束索引则与fill()使用同样的计算方法。

let ints, reset = () => ints = [0,1,2,3,4,5,6,7,8,9];
reset();

ints.copyWith(5);	// 复制0开始的内容,插到5开始的位置
console.log(ints);	// [0,1,2,3,4,0,1,2,3,4]
reset();

ints.copyWith(0,5);	// 复制5开始的内容,插到0开始的位置
console.log(ints);	// [5,6,7,8,9,5,6,7,8,9]
reset();

ints.copyWith(4,0,3);	// 复制从0~3的内容(不包括3的内容),插到4开始的位置
console.log(ints);	// [0,1,2,3,0,1,2,7,8,9]
reset();

ints.copyWith(-4,-7,-3);	// 支持负索引值,与fill()相对于数组末尾计算正向索引的过程是一样的
// -4+ints.length = 6	插入到6开始的位置
// -7+ints.length = 3; -3+ints.length = 7	复制从3~7的内容(不包含7的内容)
alert(ints);	// [0,1,2,3,4,5,3,4,5,6] 

填充数组方法fill()

可以向一个已有的数组中插入全部或部分相同的值。开始索引用于指定开始填充的位置,它是可选的。负值索引从数组末尾开始计算。

const zeroes = [0,0,0,0,0];

zeroes.fill(6,3);	// 用6填充索引大于等于3的元素
console.log(zeroes);		// [0,0,0,6,6];

zeroes.fill(7,1,3);	// 用7填充索引大于等于1且小于3的元素
console.log(zeroes);		// [0,7,7,0,0];

zeroes.fill(8,-4,-1);	// 用8填充索引大于等于1且小于4的元素
// (-4 +zeroes.length = 1)
// (-1 +zeroes.length = 4)
console.log(zeroes);	// [0,8,8,8,0]

转换方法

对数组的每个值都会调用其toString()方法,以得到最终的字符串。

valueOf()返回的还是数组本身, 而toString()返回由数组中每个值的等效字符串拼接而成的一个逗号字符串。 toLocaleString()方法与另外两个方法,唯一的区别是,为了得到最终字符串。会调用数组每个值的toLocaleString()方法。

如果想使用不同的分隔符,可以使用join()方法,接受一个参数,即字符串分隔符,返回包含所有项的字符串。

let colors = ["red","green","blue"];
alert(colors.join("|"));	// red|green|blue

若不给join()传入任何参数,或者传入undefined,则仍然使用逗号分隔符。 注意,若数组中某一项是null或undefined,则在使用上面方法时返回的结果以空字符串表示

栈方法

栈以先进后出形式限制访问,ES数组提供了push()和pop()方法,以实现类似栈的行为。

push()

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

pop()

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

队列方法

队列以先进先出形式限制访问,在队列末尾添加数据,在队列开头获取数据。使用shift()和push(),可以把数组当作队列来使用。

push()

在数据末尾添加数据。

shift()

会删除数组的第一项并返回它,然后数组长度-1。

unshift()

在数组开头添加任意多个值,然后返回新的数组长度。 通过使用unshift()和pop(),可以在数组开头添加数据,在数组末尾取得数据。

排序方法

reverse()

将数组元素反向排列。

sort()

按升序重新排列数组元素。会在每一项上调用String()转型函数,然后比较字符串来决定顺序。 sort()可以接收一个比较函数,用于判断哪个值应该排在前面,接受两个参数,若参数1>参数2,就返回负值;若相等,则返回0;若参数1<参数2,返回正值。 注意,reverse()和sort()都返回调用它们的数组的引用。**

操作方法

concat()

在现有数组全部元素基础上创建一个新数组。

let colors = ["red","grey","blue"];
let colors2 = colors.concat("yellow",["black","brown"]);

console.log(colors2);	// ["red","grey","blue","yellow","black","brown"]

slice()

用于创建一个包含原有数组中一个或多个元素的新数组。可接受一个或两个参数:返回元素的开始索引和结束索引。 这个操作不影响原始数组。

let idols = ["jk","cln","wjk","yqw"];
let idol1 = idols.slice(1);
let idol2 = idols.slice(1,3);

alert(idol1);	// cln,wjk,yqw
alert(idol2);	// cln,wjk 	  不包含结束索引3位置的元素

若参数有负值,则就以数值长度+这个负值的结果来确定位置。

splice()

在数组中间插入元素,splice()方法始终返回这样一个数组,包含从数组中被删除的元素(若没有删除元素,则返回空数组)。可执行三种操作:

(1)删除

可以从数组中删除任意多个元素,需传入2个参数:要删除的第一个元素的位置和要删除的元素数量。

(2)插入

可以在数组中指定的位置插入元素。需传入3个参数:开始位置、0(要删除的元素数量)和要插入的元素。第三个个参数之后可以传任意多个要插入的元素。

(3)替换

在删除元素的同时可以在指定位置插入新元素。需传入3个参数:开始位置、要删除元素的数量和要插入的任意多个元素。要插入的元素数量不一定跟删除的元素数量一致。

let colors = ["red","green","blue"];
let removed = colors.splice(0,1);	//删除位置0的一个元素,并返回此元素
alert(colors);	// green,blue
alert(removed);	// red,只有一个元素的数组

removed = colors.splice(10,"yellow","orange");	//在位置1插入两个新元素 此时没有需删除的元素,返回空数组
alert(colors);	// green,yellow,orange,blue
alert(removed);	// 空数组

removed = colors.splice(11,"red","purple");	//在位置1的元素插入两个删除位置0的元素,并返回此元素
alert(colors);	// green,red,perple,orange,blue
alert(removed);	// yellow,只有一个元素的数组

搜索和位置方法

ES提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索。

严格相等

ES提供3个严格相等的搜索方法:indexOf()、lastIndexOf()和includes()【ES7新增的】。

前两个方法在所有版本中都可用,而第三个方法是ES7新增的。

都接收两个参数:要查找的元素和一个可选的起始搜索位置。

indexOf()和includes()方法从数组前头开始向后搜索,而lastIndexOf()从数组末尾开始向前搜索。

indexOf()和lastIndexOf()都返回要查找的元素在数组中的位置,若没找到则返回-1。 includes()返回布尔值,表示是否找到相匹配的元素。

在比较第一个参数跟数组每一项时,会使用全等(===)比较,也就是说两项必须严格相等。

let person = {name: "Leo"};
let people = [{name: "Leo"}];
let morePeople = [person];

alert(people.indexOf(person));		// -1  person中的元素是Leo,people中的元素是name:"Leo"
alert(morePeople.indexOf(person));	// 0 两者相等
alert(people.includes(person));	// false
alert(morePeople.includes(person));	// true

断言函数

ES也允许按照定义的断言函数搜索数组,每个索引都会调用这个函数。断言函数值决定了相应索引的元素是否被认为匹配。

接收3个参数:数组中当前搜索的元素、当前元素的索引和正在搜索的数组本身。返回真值,表示是否匹配。

find()和findIndex()方法使用断言函数,都是从数组的最小索引开始。

find()返回第一个匹配的元素;findIndex()返回第一个匹配元素的索引。

都可接收第二个可选的参数,用于指定断言函数内部this的值。

const evens = [2,4,6];

evens.find((element, index, array) => {
	console.log(element);
	console.log(index);
	console.log(array);
	return element === 4;
});
// 2
// 0
// [2,4,6]
// 4
// 1
// [2,4,6]

找到匹配项后,这两个方法都不再继续搜索。

迭代方法

ES为数组定义了5个迭代方法,都是对数组每一项都运行传入的函数。都接收两个参数:以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中this的值)。

传给每个方法的函数接收3个参数:数组元素、元素索引和数组本身。

every()

对每一项函数都返回true,则这个方法返回true。

filter()

可以将数组中不符合的元素去掉,返回一个新数组。 函数返回true的项会组成数组之后返回。

var arr = [1, 2, 2, 3, 4, 5, 5, 6, 7, 7,8,8,0,8,6,3,4,56,2];
var arr2 = arr.filter((x, index,self)=>self.indexOf(x)===index)
console.log(arr2); //[1, 2, 3, 4, 5, 6, 7, 8, 0, 56]

当要过滤的数组是对象数组时,修改新返回的对象数组属性时,同时也会修改原来的对象数组

let arr = [{ id : 1, name : cln},
    { id : 2, name : jk},
    { id : 3, name : lms},
    { id : 4, name : yqw}
];
let res = arr.filter((element, index) => {
    return element.id %2 ===0;
});
res[0].name = 123;
console.log(arr);
// { id : 1, name : cln}
// { id : 2, name : 123}
// { id : 3, name : lms}
// { id : 4, name : yqw}
console.log(res);
// { id : 2, name : 123}
// { id : 4, name : yqw}

当过滤的数组是纯数组时,修改过滤后的数组,原来的数组不会变

let arr = [1, 2, 3, 4, 5, 6];
let res = arr.filter((element, index) => {
    return element % 2 === 0;
});
res[0] = 8;
console.log(arr);
// [1, 2, 3, 4, 5, 6]
console.log(res);
// [8, 4, 6]

forEach()

没有返回值。相当于使用for循环遍历数组。

map()

返回由每次函数调用的结果构成的数组。适合创建一个与原数组元素一一对应的新数组。

some()

如果有一项函数返回true,则这个方法返回true。

归并方法

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

都接收两个参数:对每一项都会运行的归并函数,以及可选的以之为归并起点的初始值。

传给reduce()和reduceRight()的函数接收4个参数:上一个归并值、当前项、当前项的索引和数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。

传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项。

reduce()

从数组第一项开始遍历到最后一项。可执行累加数组中所有数值的操作。

let values = [1,2,3,4,5];
let sum = values.reduce( (prev, cur, index, array) => prev + cur);

alert(sum);	// 15  第一次执行reduce()时,prev=1,cur=2;第二次执行时prev=3(1+2),cur=3(数组第三项),以此类推。

reduceRight()

从最后一项开始遍历到第一项。与reduce()方法类似,只是方向相反。

定型数组

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

ArrayBuffer

是所有定型数组及视图引用的基本单位。Float32Array实际上是一种“视图”,可以允许JavaScript运行时访问一块名为ArrayBuffer的预分配内存。

SharedArrayBuffer 是ArrayBuffer的一个变体,可以无须复制就在执行上下文间传递它。

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

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

const buf1 = new ArrayBuffer(16);
const buf2 = buf1.slice(4,12);
alert(buf2.byteLength);	// 8

在某种程度上类似于C++的malloc(),也有几个明显的区别:

  1. malloc()在分配失败时会返回一个null指针。而ArrayBuffer在分配失败时会抛出错误;
  2. malloc()可以利用虚拟内存,因此最大可分配尺寸只受可寻址系统内存限制。而AB分配的内存不超过(2^53-1)字节。
  3. malloc()调用成功不会初始化实际地址,而声明AB则会将所有二进制位初始化为0。
  4. 通过malloc()分配的堆内存只能调用free()或程序退出,而通过AB分配的堆内存可以被当成垃圾回收,无需手动释放。

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

视图

DataView

允许读写ArrayBuffer的视图,专为文件I/O和网络I/O设计,其API支持对缓冲数据的高度控制。对缓冲内容没有任何预设,也不能迭代。

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

const buf = new ArrayBuffer(16);

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

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

// 若不指定,则DataView会使用剩余的缓冲
const secondHalfDataView = new DataView(buf, 8);	// 8表示视图从缓冲的第9个字节开始,
alert(firstHalfDataView.byteOffset);	// 8
alert(firstHalfDataView.byteLength);	// 8
alert(firstHalfDataView.buffer === buf);	// true

要通过DataView读取缓冲,需要以下组件:(1)要读或写的字节偏移量;(2)使用ElementType来实现JS的Number类型到缓冲内二进制格式转换;(3)内存中值的字节序,默认为大端字节序。

1. ElementType

DataView对存储在缓冲内的数据类型没有预设。 ES6支持8中不同的ElementType:

image.png

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

DataView 会自动将数据转换为特定的ElementType。

2. 字节序

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

DataView只支持两种约定:大端字节序和小端字节序。大端字节序也称为为“网络字节序”,即最高有效位保存在第一个字节,而最低有效位保存在最后一个字节。小端字节序正好相反。

对一段内存而言,DataView是一个中立接口,它会遵循你指定的字节序。DataView的所有API方法都以大端字节序作为默认值,但接收一个可选的布尔值参数,设置为true即可启用小端字节序。

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

// 填充缓冲,让第一位和最后一位都是1
view.setUint8(0, 0x80);	
view.setUint8(1, 0x01);	

// 按大端字节序读取Uint16
// 0x80是高字节 0x01是低字节
// 0x8001 = 2^15+2^0 = 32769
alert(view.getUint16(0));	// 32769

// 按小端字节序读取Uint16
// 0x01是高字节 0x80是低字节
// 0x0180 = 2^8+2^7 = 384
alert(view.getUint16(0,true));	// 384

// 按大端字节序写入 Uint16
view.setUint16(0, 0x0004);
// 按小端字节序写入 Uint16
view.setUint16(0, 0x0002, true);
3. 边界情形

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

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

定型数组

是另一种形式的ArrayBuffer视图。特定于一种ElementType且遵循系统原生的字节序。

创建定型数组的方式:读取已有的缓冲、使用自有缓冲、填充可迭代结构、以及填充基于任意类型的定型数组。

通过<ElementType>.from()和<ElementType>.of()也可创建定型数组。

const buf = new ArrayBuffer(12);
const ints = new Int32Array(buf);
alert(ints.length);	// 3, 定型数组知道每个元素需要4字节【32/8=4】,长度12/4=3。

const ints2 = new Int32Array(6);
alert(ints2.length);	// 6
alert(ints2.buffer.byteLength);	// 24, 每个元素需要4字节,因此ArrayBuffer是24字节【6*4】

// 基于普通数组创建
const ints3 = Int16Array.from([3,5,7,9]);
alert(ints3.length);	// 4
alert(ints3.buffer.byteLength);	// 8, 每个元素需要2字节,因此ArrayBuffer是8字节
alert(ints3[2]);	// 7

// 基于传入参数创建
const floats = Float32Array.of(3.14, 2.718, 1.618);
alert(floats.length);	// 3
alert(floats.buffer.byteLength);	// 12, 每个元素需要4字节,因此ArrayBuffer是12字节
alert(floats[2]);	// 1.6180000305175781

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

alert(Int16Array.BYTES_PER_ELEMENT);	// 2
alert(Int32Array.BYTES_PER_ELEMENT);	// 4

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

1. 定型数组行为

返回新数组的方法也会返回包含同样元素类型的新定型数组:

const ints = new Int16Array([1,2,3]);
const doubleints = ints.map( x => 2*x);
alert(doubleints instanceof Int16Array );	// true

有一个Symbol.iterator 符号属性,因此可以通过for..of 循环和扩展操作符来操作:

const ints = new Int16Array([1,2,3]);
for(const int of ints){
	alert(int);
}
// 1
// 2
// 3

alert(Math.max(...ints));	// 3
2. 合并、复制和修改定型数组

同样使用数据缓冲来存储数据,而数据缓冲无法调整大小。因此无法使用concat()、pop()、push()、shift()、splice()和unshift()。 提供了两个新方法,可以快速向外或向内复制数据。

set()

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

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

container.set([5,6,7,8], 4);		// 把普通数组复制为后4个值,偏移量4表示从索引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]

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

const paticalCopy = source.subarray(1,3);	// 从索引1开始复制到索引3【不包含结束索引】
console.log(paticalCopy);		// [4,6]

定型数组没有原生的拼接能力,但使用定型数组API提供的很多工具可以手动搭建。

3. 下溢和上溢

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

Map

新的集合类型,为ES带来了真正的键/值存储机制。

基本API

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

如果想在创建的同时初始化实例,可以给Map构造函数传入一个可迭代对象,需包含键/值对数。

const m1 = new Map([
	["key1","val1"],
	["key2","val2"],
	["key3","val3"]
]);
alert(m1.size);	// 3

const m2 = new Map([ [ ] ]);
alert(m2.has(undefined));	// true
alert(m2.get(undefined));	// undefined

set()添加键/值对,返回映射实例 get()和has()进行查询, 通过size属性获取映射中的键/值对的数量, delete()和clear()删除值。

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

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"]

顺序与迭代

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

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

const m = new Map([
	["key1", "val1"],
	["key2", "val2"],
	["key3", "val3"]
]);

alert(m.entries === m[Symbol.iterator]);	// true

for( let pair of m.entries())	{
	alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]

for( let pair of m[Symbol.iterator]() ){
	alert(pair);
}

keys()和values()分别返回以插入顺序生成键和值的迭代器:

const m = new Map([
	["key1", "val1"],
	["key2", "val2"],
	["key3", "val3"]
]);

for( let key of m.keys()) {
	alert(key);
}
for( let value of m.values()) {
	alert(value);
}

键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改:

const m1 = new Map([	["key1", "val1"] 	]);

// 作为键的字符串原始值是不能修改的
for( let key of m1.keys()){
	key = "newKey";
	alert(key);		// newKey
	alert(m1.get("key1"));	// val1
}

const keyObj = {	id : 1 };
const m = new Map([	[keyObj, "val1"]	]);

// 修改了作为键的对象的属性,但对象在映射内部仍然引用相同的值。
for( let key of m.keys()){
	key.id = "newKey";
	alert(key);		// { id : "newKey"}
	alert(m.get(keyObj));	// val1
}
alert(keyObj);	// { id : "newKey"}

Object与Map之间的选择

1. 内存占用

给定固定大小的内存,Map大约可以比Object 多存储50%的键/值对。

2. 插入性能

插入Map在所有浏览器中一般会稍微快一点儿。如果代码涉及大量插入操作,显然Map的性能更佳。

3. 查找速度

如果只包含少量键/值对,则Object有时候速度更快。如果代码涉及大量查找操作,在某些情况下可能选择Object更好一些。

4. 删除性能

Map的delete()操作都比插入和查找更快。如果代码涉及大量删除操作,则毫无疑问应该选择Map。

WeakMap

ES6新增的“弱映射”(WeakMap)是一种新的集合类型。是Map的“兄弟”类型,其API也是Map的子集。WeakMap中的“weak”,描述的是JS垃圾回收程序对待“弱映射”中键的方式。

基本API

使用new 关键字实例化一个空的 WeakMap,弱映射中的键只能是Object或者继承自Object的类型。值的类型无限制。尝试使用非对象设置键会抛出TypeError。

初始化时构造函数可接收一个可迭代对象,其中需包含键/值对数组。只要有一个键无效就会抛出错误,导致整个初始化失败。 原始值可以先包装成对象再用作键:

const stringKey = new String("key1");
const wm3 = new WeakMap([
	stringKey , "val1"
]);
alert(wm3.get(stringKey));	// "val1"

初始化后可使用set()添加键/值对,返回弱映射实例。可以使用get()和has()查询,delete()删除。

弱键

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

不可迭代键

因为WeakMap中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。

使用弱映射

WeakMap实例与JS对象有着很大不同。

  1. 私有变量 私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。

可以用一个闭包把WeakMap包装起来,就可以把弱映射与外界完全隔离开了:

  1. DOM节点元数据 因为WeakMap实例不会妨碍垃圾回收,所以非常适合保存关联元数据

当节点从DOM树中被删除后,垃圾回收可以立即释放其内存(假设没有其他地方引用此对象):

const wm = new WeakMap();
const loginButton = document.querySelector('#login');
wm.set(loginButton, {disabled: true});	// 给这个节点关联一些元数据

Set【ES6新增】

ES6新增的集合类型,在很多方面像是加强的Map,因为它们的大多数API和行为都是共有的。

基本API

使用new关键字和Set 构造函数可以创建一个空集合。

const s = new Set();

初始化之后可以使用add()增加值,返回集合实例;has()查询,size()取得元素数量,以及delete()和clear()删除元素。delete返回布尔值(代表集合中是否存在需要删除的元素)。

与Map类型一样,Set可以包含任何JavaScript数据类型作为值。

与严格相等一样,用作值的对象和其他“集合"类型在自己的内容或属性被修改时也不会改变。

顺序与迭代

Set会维护值插入时的顺序,因此支持按顺序迭代

集合实例可以提供一个迭代器,能以插入顺序生成集合内容。可以通过values()方法以及keys()(或者Symbol.iterator属性,他引用values())取得这个迭代器。

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

const s = new Set(["val1","val2","val3"]);
console.log([...s]);
// ["val1","val2","val3"]

集合的entries()方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元素是集合中每个值的重复出现。

for(let pair of s.entries()){
    console.log(pair);
}
// ["val1","val1"]
// ["val2","val2"]
// ["val3","val3"]

如果不使用迭代器,而是使用回调方式,则可调用集合的forEach()方法并传入回调,依次迭代每个键/值对。

s.forEach((val, dupVal) => alert(`${val} -> ${dupVal}`));
// val1 -> val1
// val2 -> val2
// val3 -> val3

修改集合中值的属性不会影响其作为集合值的身份,与Map类似。

操作方法:

Set中存放的是不重复的数据。

  1. 数组与Set之间的转换
var arr = ["1","2","1","2","3","1"];
var set = new Set(arr);
// 得到一个新set:{"1","2","3"}

var arr1 = Array.from(set);
// 得到一个新数组:["1","2","3"]
  1. 使用Set给数组去重
var arr = ["1","2","1","2","3","1"];
// 去重方法一
var arr1 = [...new Set(arr)];
// 去重方法二
var arr2 - Array.from(new Set(arr));
  1. 求两个Set的并集、交集和差集
var arr1 = ["1","2","3"];
var arr2 = ["1","2"];
var set1 = new Set(arr1);
var set2 = new Set(arr2);

// 求并集
var newSet1 = new Set([...set1,...set2]);
// 求交集
var newSet2 = new Set([...set1].filter(x => set2.has(x)));
// 求差集
var newSet3 = new Set([...set1].filter(x => !set2.has(x)));

WeakSet

ES6新增的集合类型——”弱集合“,是Set的“兄弟”类型,其API也是Set的子集。WeakSet中的“weak”,描述的是JS垃圾回收程序对待“弱映射”中键的方式。

基本API

使用new关键字实例化,弱映射中的键只能是Object或者继承自Object的类型。值的类型无限制。尝试使用非对象设置键会抛出TypeError。 初始化时构造函数可接收一个可迭代对象,其中需包含键/值对数组。只要有一个键无效就会抛出错误,导致整个初始化失败。 原始值可以先包装成对象再用作键。

初始化之后add()添加新值,has()查询,delete()删除元素。

弱值

这些值不属于正式的引用,不会阻止垃圾回收。当代码执行完成后,这个对象值被当作垃圾回收,然后这个值就从弱集合中消失,使其成为一个空集合。

不可迭代键

因为WeakMap中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。

使用弱集合

弱集合在给对象打标签时还是有价值的。

const disabledElements = new WeakSet();
const loginBtn = document.querySelector('#login');
disabledElement.add(loginBtn);	// 通过加入对应集合,给此节点打上”禁用“标签。

只要WeakSet 中任何元素从DOM树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存(假设没有其他地方引用这个值)。

迭代与扩展操作

4种原生集合类型定义了默认迭代器: Array、所有定型数组、Map、Set。 即上述所有类型都支持顺序迭代,都可传入for-of 循环。

所有这些类型都兼容扩展操作符,在对可迭代对象执行浅复制时特别有用,只需简单语法即可复制整个对象

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 arr1 = [{}];
let arr2 = [...arr1];

arr1[0].foo = 'bar';
console.log(arr2[0]);	// { foo : 'bar' }