侧边栏壁纸
博主头像
lac博主等级

行动起来,活在当下

  • 累计撰写 66 篇文章
  • 累计创建 12 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

js红书读书笔记——第六章 集合引用类型

Hude
2023-12-19 / 0 评论 / 0 点赞 / 104 阅读 / 67590 字

第六章 集合引用类型

Object

Object 是 ECMAScript 中最常用的类型之一,适合存储或在应用程序之间交换数据

显式创建Object类型的两种方式:

  • 使用构造函数

const obj = new Object();
obj.name = 'xiaoyao';
obj.age = 18;
  • 使用对象字面量(object literal)表示法

const obj = {
name:'xiaoyao',
age:18
}

在 ECMAScript 中,表达式上下文指的是期待返回值的上下文。赋值操作符表示后面要期待一个值,因此左大括号表示一个表达式的开始。同样是左大括号,如果出现在语句上下文(statementcontext)中,比如 if 语句的条件后面,则表示一个语句块的开始

最后一个属性后面加上逗号在非常老的浏览器中会导致报错,但所有现代浏览器都支持这种写法

对象字面量表示法如果不传入任何属性,那么它和使用构造函数创建的对象是一致的

数值属性会自动转换为字符串

let person = {}; // 与 new Object()相同
let obj = {5:true}; // 注意,数值属性会自动转换为字符串

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

对象字面量是函数传递大量可选参数的主要方式:

function displayInfo(args) { 
 let output = ""; 
 if (typeof args.name == "string"){ 
 output += "Name: " + args.name + "\n"; 
 } 
 if (typeof args.age == "number") { 
 output += "Age: " + args.age + "\n"; 
 } 
 alert(output); 
} 
displayInfo({ 
 name: "Nicholas", 
 age: 29 
}); 
displayInfo({ 
 name: "Greg" 
});

注意,如果传递的参数过多,也可能会造成结构体笨重,最好的方式是必选参数用命名参数,而可选参数使用对象字面量方式传递

存取属性的两种方式:

  • 点语法:ObjectName.KeyName

  • 中括号法:ObjectName[keyName],注意keyName只能是字符串

这两种存取属性的方式本质上没有区别,使用中括号的优势是可以使用变量访问属性

const p = {
    name:'p',
    age:18
}
const n = 'name';
console.log(p[n]);// p

另外,中括号法还可以存取一些命名不规范的属性

const obj = {};
obj['oip name'] = 'oipName';

Array

ECMAScript描述的数组是一组有序的集合。但是它与其他语言中的数组有着很大的区别:

  • 数组中的槽位可以存储任意类型的数据

  • 数组的长度是动态变化的,会随着数据添加而动态增长

1.创建数组

方式一:通过构造函数

语法:new Array(arg);

arg有两种可选值:

可以传入数值,表示创建数组中元素的数量

let colors = new Array(20);

可以传入数组元素

let colors = new Array('red','blue','yellow');

可以省略new关键字,因为Array是window对象的一个属性

let colors = Array(3);

方式二:通过数组字面量

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

const colors = ['red','blue','yellow'];

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

of()方法和new Array()方法的区别:

1.它们都能通过传入多个参数来创建数组,传入的参数做数组元素

2.传入单个参数时,Array表示的是元素个数,而of()方法始终表示元素

3.of()方法更倾向于把传入参数做元素来处理,无论传入的是什么参数,而Array则会根据参数来做出相应的处理

方式三:ES6新增的创建数组的静态方法:form()和of()

from()用于将类数组结构转换为数组实例,而 of()用于将一组参数转换为数组实例

from()方法:

第一个参数是一个类数组结构,也就是任何可迭代对象,或者有一个 length 属性和可索引元素的结构

Array.from('abc');
// ['a', 'b', 'c']
Array.from({length:3});
// [undefined, undefined, undefined]
// 字符串会被拆分为单字符数组
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] 
// Array.from()对现有数组执行浅复制
const a1 = [1, 2, 3, 4]; 
const a2 = Array.from(a1); 
console.log(a1); // [1, 2, 3, 4] 
alert(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] 
// from()也能转换带有必要属性的自定义对象
const arrayLikeObject = { 
 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 的值。但这个重写的 this 值在箭头函数中不适用。

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

Array.of()可以把一组参数转换为数组。这个方法用于替代在 ES6之前常用的 Array.prototype.slice.call(arguments),一种异常笨拙的将 arguments 对象转换为数组的写法:

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

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

2.数组空位

使用数组字面量创建数组时,可以使用一串逗号来创建空位(hole)。

ECMAScript会将逗号之间相应的位置的值当成空位

const option = [,,,,,,];
console.log(option.length);// 6
console.log(option);// [空属性x6]

ES6 新增的方法和迭代器与早期 ECMAScript 版本中存在的方法行为不同。ES6 新增方法普遍将这些空位当成存在的元素,只不过值为 undefined:

const option = [,,2,,,4,];
for(item of option){
    console.log(item === undefined);
}
// true true false true true false
const a = Array.from([,,,]); // 使用 ES6 的 Array.from()创建的包含 3 个空位的数组
for (const val of a) { 
 alert(val === undefined); 
} 
// true 
// true 
// true 
alert(Array.of(...[,,,])); // [undefined, undefined, undefined] 
for (const [index, value] of options.entries()) { 
 alert(value); 
} 
// 1 
// undefined 
// undefined 
// undefined 
// 5

ES6 之前的方法则会忽略这个空位,但具体的行为也会因方法而异

const options = [1,,,,5]; 
// map()会跳过空位置
console.log(options.map(() => 6)); // [6, undefined, undefined, undefined, 6] 
// join()视空位置为空字符串
console.log(options.join('-')); // "1----5"

注意,由于行为不一致和存在性能隐患,因此实践中要避免使用数组空位。如果确实需要空位,则可以显式地用 undefined 值代替。

3.数组索引

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

const arr = [1,2,3,4,5];
console.log(arr[0]);
arr[5] = 6;
console.log(arr);
// 1
// (6) [1, 2, 3, 4, 5, 6]

数组中元素的数量保存在 length 属性中,这个属性始终返回 0 或大于 0 的值

初次之外,由于length不是只读值,因此length属性还可以用于删除值:

const arr = [1,2,3,4,5];
arr.length = 4;
console.log(arr);
// VM48066:3 (4) [1, 2, 3, 4]

如果length值大于数组元素的值,则会以undefined填充

使用length值可以快速在数组末尾添加值

const arr = [1,2,3,4,5];
arr[arr.length] = 6;
console.log(arr);
// VM1500:3 (6) [1, 2, 3, 4, 5, 6]

数组的长度最大值为4 294 967 295,超过这个值就会抛出错误,以这个最大值创建数组,可能会导致脚本运行时间过长的错误

4.检测数组

一个经典的 ECMAScript 问题是判断一个对象是不是数组。

在只有一个网页(因而只有一个全局作用域)的情况下,使用 instanceof 操作符就足矣

if(value instanceof Array){
}

使用 instanceof 的问题是假定只有一个全局执行上下文。如果网页里有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数

如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组

为了解决这个问题,ECMAScript提供了Array.isArray()方法

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

if(Array.isArray(value)){}

5.迭代器方法

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

  • keys()方法:返回数组索引的迭代器

  • values()方法:返回数组元素的迭代器

  • entries()方法:返回索引/值对的迭代器

const a = ["foo", "bar", "baz", "qux"];
const keys = Array.from(a.keys());
const values = Array.from(a.values());
const kv = Array.from(a.entries());
console.log(keys);
console.log(values);
console.log(kv);
// (4) [0, 1, 2, 3]
// (4) ['foo', 'bar', 'baz', 'qux'] 
// (4) [Array(2), Array(2), Array(2), Array(2)]

由于返回的是迭代器,可以使用from方法直接转换为数组实例

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

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

注意,虽然这些方法是 ES6 规范定义的,但在 2017 年底的时候仍有浏览器没有实现它们

6.复制和填充方法

ES6 新增了两个方法:

  • fill()方法:填充方法

  • copyWithin()方法:批量复制方法

这两个方法的函数签名类似,都需要指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。使用这个方法不会改变数组的大小

fill()方法可以向一个已有的数组中插入全部或部分相同的值。fill()方法接收三个参数,第一个参数是要填充的数据,第二个参数表示填充的起始位置,第三个参数表示填充的结束位置

第二、三个参数是可选参数,不填则默认从索引0出开始填充,可以填负数,负值索引从数组末尾开始计算

可以将负索引想象成数组长度加上它得到的一个正索引=>len + (-n);

const zeroes = [0, 0, 0, 0, 0]; 
// 用 5 填充整个数组
zeroes.fill(5); 
console.log(zeroes); // [5, 5, 5, 5, 5] 
zeroes.fill(0); // 重置
// 用 6 填充索引大于等于 3 的元素
zeroes.fill(6, 3); 
console.log(zeroes); // [0, 0, 0, 6, 6] 
zeroes.fill(0); // 重置
// 用 7 填充索引大于等于 1 且小于 3 的元素
zeroes.fill(7, 1, 3); 
console.log(zeroes); // [0, 7, 7, 0, 0]; 
zeroes.fill(0); // 重置
// 用 8 填充索引大于等于 1 且小于 4 的元素
// (-4 + zeroes.length = 1) 
// (-1 + zeroes.length = 4) 
zeroes.fill(8, -4, -1); 
console.log(zeroes); // [0, 8, 8, 8, 0];

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

// 索引过低,忽略
zeroes.fill(1, -10, -6); 
console.log(zeroes); // [0, 0, 0, 0, 0] 
// 索引过高,忽略
zeroes.fill(1, 10, 15); 
console.log(zeroes); // [0, 0, 0, 0, 0] 
// 索引反向,忽略
zeroes.fill(2, 4, 2); 
console.log(zeroes); // [0, 0, 0, 0, 0] 
// 索引部分可用,填充可用部分
zeroes.fill(4, 3, 10) 
console.log(zeroes); // [0, 0, 0, 4, 4]

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

开始索引和结束索引则与 fill()使用同样的计算方法

copyWithin()方法接收三个参数:第一个参数开始复制的索引位置,第二个参数表示从第几个索引处开始复制,第三个参数表示复制结束的索引位置

let ints, 
 reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 
reset(); 
// 从 ints 中复制索引 0 开始的内容,插入到索引 5 开始的位置
// 在源索引或目标索引到达数组边界时停止
ints.copyWithin(5); 
console.log(ints); // [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] 
reset(); 
// 从 ints 中复制索引 5 开始的内容,插入到索引 0 开始的位置
ints.copyWithin(0, 5); 
console.log(ints); // [5, 6, 7, 8, 9, 5, 6, 7, 8, 9]
reset(); 
// 从 ints 中复制索引 0 开始到索引 3 结束的内容
// 插入到索引 4 开始的位置
ints.copyWithin(4, 0, 3); 
alert(ints); // [0, 1, 2, 3, 0, 1, 2, 7, 8, 9] 
reset(); 
// JavaScript 引擎在插值前会完整复制范围内的值
// 因此复制期间不存在重写的风险
ints.copyWithin(2, 0, 6); 
alert(ints); // [0, 1, 0, 1, 2, 3, 4, 5, 8, 9] 
reset(); 
// 支持负索引值,与 fill()相对于数组末尾计算正向索引的过程是一样的
ints.copyWithin(-4, -7, -3); 
alert(ints); // [0, 1, 2, 3, 4, 5, 3, 4, 5, 6] 
copyWithin()静默忽略超出数组边界、零长度及方向相反的索引范围:
let ints, 
 reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 
reset(); 
// 索引过低,忽略
ints.copyWithin(1, -15, -12); 
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 
reset() 
// 索引过高,忽略
ints.copyWithin(1, 12, 15); 
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 
reset(); 
// 索引反向,忽略
ints.copyWithin(2, 4, 2); 
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 
reset(); 
// 索引部分可用,复制、填充可用部分
ints.copyWithin(4, 7, 10) 
alert(ints); // [0, 1, 2, 3, 7, 8, 9, 7, 8, 9];

7.转换方法

所有对象都有toLocaleString()、toString()和 valueOf()方法

valueOf()方法返回数组本身

toString()方法返回由数组元素组成的以逗号分隔的字符串,在此转换期间,会对每个元素都调用toString()方法,以得到最终的结果

const colors = ['red','blue','green'];
console.log(colors.toString());
console.log(colors.valueOf());
console.log(colors);
// VM3932:2 red,blue,green
// VM3932:3 (3) ['red', 'blue', 'green']
// VM3932:4 (3) ['red', 'blue', 'green']

同样的,在调用数组的 toLocaleString()方法时,会得到一个逗号分隔的数组值的字符串,但是它的区别在于,内部对每个元素调用的都是toLocaleString()方法,而不是toString()方法

let person1 = { 
 toLocaleString() { 
 return "Nikolaos"; 
 }, 
 toString() { 
 return "Nicholas"; 
 } 
}; 
let person2 = { 
 toLocaleString() { 
 return "Grigorios"; 
 }, 
 toString() { 
 return "Greg"; 
 } 
}; 
let people = [person1, person2]; 
alert(people); // Nicholas,Greg 
alert(people.toString()); // Nicholas,Greg 
alert(people.toLocaleString()); // Nikolaos,Grigorios

toString()和toLocaleString()方法都是默认以逗号为分割返回字符串,如果想要返回不同的分隔符,可以使用join()方法

join()方法:传入一个字符串参数,返回数组元素以传入参数为分隔符的字符串,如果不传入参数,或者传入undefined,默认还是使用逗号作为分隔符

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

注意,如果数组中某一项是 null 或 undefined,则在 join()、toLocaleString()、toString()和 valueOf()返回的结果中会以空字符串表示

ECMAScript 给数组提供几个方法,让它看起来像是另外一种数据结构

8.栈方法

栈是一种后进先出(LIFO,Last-In-First-Out)的结构,也就是最近添加的项先被删除。数据项的插入(称为推入,push)和删除(称为弹出,pop)只在栈的一个地方发生,即栈顶

使用 pop()和 push(),可以把数组当成栈来使用

push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。pop()方法则用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项

let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2 
count = colors.push("black"); // 再推入一项
alert(count); // 3 
let item = colors.pop(); // 取得最后一项
alert(item); // black 
alert(colors.length); // 2

9.队列方法

队列以先进先出(FIFO,First-In-First-Out)形式限制访问。队列在列表末尾添加数据,但从列表开头获取数据。从尾部添加数据已经有了push()方法,因此ECMAScript还提供了shift()方法来删除头部数据

使用 shift()和 push(),可以把数组当成队列来使用

let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2 
count = colors.push("black"); // 再推入一项
alert(count); // 3 
let item = colors.shift(); // 取得第一项
alert(item); // red 
alert(colors.length); // 2

除了shift()方法,ECMAScript还提供了unshift()方法,它与shift()方法相反,是在队列头部添加数据,unshift()方法和pop()方法可以在相反方向上组成队列。

同时,push()、shift()、pop()、unshift()这几个方法还可以组成双端队列

let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2 
count = colors.unshift("black"); // 再推入一项
alert(count); // 3 
let item = colors.pop(); // 取得最后一项
alert(item); // green 
alert(colors.length); // 2

10.排序方法

数组提供两个方法用于数组排序:

  • reverse()方法:反向排序

  • sort()方法:灵活排序,提供回调函数用于指定排序规则

const nums = [1,2,3,4,5,6]
nums.reverse();
// (6) [6, 5, 4, 3, 2, 1]

默认情况中,sort()方法会正向排序,但是由于sort()方法比较元素是通过将元素转换为String类型,因此在元素值超过10时排序将不会按照正向排序。

换句话说sort()方法不默认排序数字,而是默认比较字符串后排序

sort()会在每一项上调用 String()转型函数,然后比较字符串来决定顺序。即使数组的元素都是数值,也会先把数组转换为字符串再比较、排序

let values = [0, 1, 5, 10, 15]; 
values.sort(); 
alert(values); // 0,1,10,15,5

具体来说,在每一项比较时,实际上是比较它们的第一个字符的 Unicode 码点

sort()方法可以接收一个比较函数,用于判断哪个值应该排在前面,比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相等,就返回 0;如果第一个参数应该排在第二个参数后面,就返回正值

let values = [0, 1, 5, 10, 15]; 
values.sort((a, b) => a < b ? 1 : a > b ? -1 : 0); 
alert(values); // 15,10,5,1,0

// 如果元素都是数值或者Date对象时
const nums = [5,4,3,2,1];
nums.sort((a,b)=>a-b);
// (5) [1, 2, 3, 4, 5]

注意,reverse()和 sort()都返回调用它们的数组的引用

11.操作方法

concat()方法在现有基础上创建一个新数组

concat()方法首先会创建当前数组的副本,然后再把自己的参数加入到副本的末尾最后返回这个新数组

如果传入一个或多个数组,则 concat()会把这些数组打平到新数组中,如果参数不是数组,就直接添加到新的数组中

let colors = ["red", "green", "blue"]; 
let colors2 = colors.concat("yellow", ["black", "brown"]); 
console.log(colors); // ["red", "green","blue"] 
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]

打平数组参数的行为可以重写,方法是在参数数组上指定一个特殊的符号:Symbol.isConcatSpreadable。这个符号能够阻止 concat()打平参数数组。相反,把这个值设置为 true 可以强制打平类数组对象

slice()方法:用于创建一个包含原有数组中一个或多个元素的新数组

接收一或两个参数,表示返回元素的开始索引和结束索引。只传入一个参数时,表示返回从这个参数开始一直到结尾的新数组

let colors = ["red", "green", "blue", "yellow", "purple"]; 
let colors2 = colors.slice(1); 
let colors3 = colors.slice(1, 4); 
alert(colors2); // green,blue,yellow,purple 
alert(colors3); // green,blue,yellow

注意,如果 slice()的参数有负值,那么就以数值长度加上这个负值的结果确定位置。

splice():主要用于在数组中插入值,其他用法有删除、替换

  • 插入:需要给 splice()传至少 3 个参数:开始位置、0(要删除的元素数量)和要插入的元素,可以在数组中指定的位置插入元素,splice()方法不局限于只插入一个值,可以在第三个参数后紧跟第四个、第五个...第N个参数

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

  • 替换:需要给splice()至少传入3个参数,要替换的第一个元素的位置,替换的元素数量,替换的元素,替换操作时替换元素数量应该等同于替换的元素,否则如果数量>元素,替换元素后会进行删除操作,如果数量<元素,替换元素后会进行插入操作

let colors = ["red", "green", "blue"]; 
let removed = colors.splice(0,1); // 删除第一项
alert(colors); // green,blue 
alert(removed); // red,只有一个元素的数组
removed = colors.splice(1, 0, "yellow", "orange"); // 在位置 1 插入两个元素
alert(colors); // green,yellow,orange,blue 
alert(removed); // 空数组
removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
alert(colors); // green,red,purple,orange,blue 
alert(removed); // yellow,只有一个元素的数组

12.搜索和位置方法

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

严格相等搜索

ECMAScript 提供了 3 个严格相等的搜索方法:indexOf()、lastIndexOf()[注:All]和 includes()[注:ES7+]

提问:在过往的ES版本中,已经提供了indexOf()这样的的严格搜索方法,那么为什么还要再后续的ES7版本中还要新增includes()方法

答:indexOf()方法在一些情况下可能存在一些问题,特别是处理NaN时,比如[NaN].indexOf(NaN)会返回-1,includes()方法的主要优势在于它更易于使用,它的返回值是非常直观的布尔值,并且在查找数组中是否包含某个元素时不受NaN的影响。

indexOf()和lastIndexOf()方法:都返回要查找的元素在数组中的位置,如果没找到则返回-1

includes()方法:返回布尔值,表示是否至少找到一个与指定元素匹配的项。

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

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1]; 
alert(numbers.indexOf(4)); // 3 
alert(numbers.lastIndexOf(4)); // 5 
alert(numbers.includes(4)); // true 
alert(numbers.indexOf(4, 4)); // 5 
alert(numbers.lastIndexOf(4, 4)); // 3 
alert(numbers.includes(4, 7)); // false 
let person = { name: "Nicholas" }; 
let people = [{ name: "Nicholas" }]; 
let morePeople = [person]; 
alert(people.indexOf(person)); // -1 
alert(morePeople.indexOf(person)); // 0 
alert(people.includes(person)); // false 
alert(morePeople.includes(person)); // true

按断言函数搜索

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

断言函数接收 3 个参数:元素、索引和数组本身

find()和 findIndex()方法使用了断言函数,它们都从第一个索引开始搜索,不同的是find()函数返回元素本身,而findIndex()返回索引

这两个方法也都接收第二个可选的参数,用于指定断言函数内部 this 的值

const people = [ 
 { 
 name: "Matt", 
 age: 27 
 }, 
 { 
 name: "Nicholas", 
 age: 29 
 } 
]; 
alert(people.find((element, index, array) => element.age < 28)); 
// {name: "Matt", age: 27} 
alert(people.findIndex((element, index, array) => element.age < 28)); 
// 0

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

13.迭代方法

ECMAScript为数组提供了5个迭代方法。对于传入参数而言,其参数类型类似搜索方法种的断言函数式。可传入两个参数:

  • 以每一项为参数运行的函数

  • 可选的作为函数运行上下文的作用域对象(影响函数中 this 的值)

其中,传给每个方法的函数接收三个参数:

  • 数组元素

  • 数组索引

  • 数组本身

5个迭代方法分别如下:

  • every():对数组每一项都调用传入的函数,如果对每一项函数都返回true,这个方法返回true,否则返回false,返回false意味着执行停止

  • filter():对数组每一项都调用传入的函数,这个方法返回每一项返回值为true的元素组成的新数组

  • forEach():对数组每一项都调用传入的函数,没有返回值

  • map():对数组每一项都调用传入的函数,返回由每一项函数调用结果组成的数组

  • some():对数组每一项都调用传入的函数,行为与every()方法相反,如果有一个方法返回true,则方法直接返回true,意味着停止执行

这些方法都不会改变原数组

some()和every()方法相似,都返回布尔值

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1]; 
let everyResult = numbers.every((item, index, array) => item > 2); 
alert(everyResult); // false 
let someResult = numbers.some((item, index, array) => item > 2); 
alert(someResult); // true

filter()方法:基于给定的函数来决定某一项是否应该包含在它返回的数组中

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1]; 
let filterResult = numbers.filter((item, index, array) => item > 2); 
alert(filterResult); // 3,4,5,4,3

map()方法:这个方法返回一个数组,这个数组的每一项都是对原始数组中同样位置的元素运行传入函数而返回的结果

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1]; 
let mapResult = numbers.map((item, index, array) => item * 2); 
alert(mapResult); // 2,4,6,8,10,8,6,4,2

forEach()方法:只对每一项元素运行传入的函数,不返回任何值,也不会修改任何元素和新增数组

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1]; 
numbers.forEach((item, index, array) => { 
 // 执行某些操作 
});

14.归并方法

ES为数组提供了两个归并方法:

  • reduce()

  • reduceRight()

这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。区别在于reduce()从第一项开始遍历,而reduceRight()从最后一项开始遍历

这两个方法都接受两个参数:

  • 对每一项都会运行的归并函数

  • 可选的以该参数为归并起点的初始值

传入的归并函数接收四个参数:

  • 上一个归并值

  • 当前值

  • 当前值的索引

  • 数组本身

注意,reduce()和reduceRight()方法没有传入起点值时,会将数组的第一个元素作为起点值,意味着,第一个元素将不会被访问,而是作为起点值传给第二个归并函数,但是索引本身不会更改,没有起点值时,索引值从1开始

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

let values = [1, 2, 3, 4, 5]; 
let sum = values.reduceRight(function(prev, cur, index, array){ 
 return prev + cur; 
}); 
alert(sum); // 15

定型数组(提升处理二进制的能力)

定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率。实际上,JavaScript 并没有“TypedArray”类型,它所指的其实是一种特殊的包含数值类型的数组

历史

早在 2006 年,Mozilla、Opera 等浏览器提供商就实验性地在浏览器中增加了用于渲染复杂图形应用程序的编程平台,无须安装任何插件。其目标是开发一套 JavaScript API,从而充分利用 3D 图形 API 和 GPU 加速,以便在<canvas>元素上渲染复杂的图形

  1. WebGL:基于OpenGL ES 2.0规范的JavaScript API 。于2011年发布1.0版。在早期版本中,因为JavaScript数组与原生数组之间不匹配,所以出现了性能问题。具体的,图形驱动程序API通常不需要以JavaScript默认双进度浮点格式传递他们的数值,而这恰恰是JavaScript数组在内存中 的格式。因此,每次WebGL与JavaScript传递数组时,WebGL绑定都需要在目标环境分配新数组,以其当前格式迭代数组,然后将数值转型为新数组中的适当格式,而这些工作要花费很多时间

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

ArrayBuffer

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

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

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

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

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

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

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

  • malloc()在分配失败时会返回一个 null 指针。ArrayBuffer 在分配失败时会抛出错误。

  • malloc()可以利用虚拟内存,因此最大可分配尺寸只受可寻址系统内存限制。ArrayBuffer

  • 分配的内存不能超过 Number.MAX_SAFE_INTEGER(253  1)字节。

  • malloc()调用成功不会初始化实际的地址。声明 ArrayBuffer 则会将所有二进制位初始化为 0。

  • 通过 malloc()分配的堆内存除非调用 free()或程序退出,否则系统不能再使用。而通过声明ArrayBuffer 分配的堆内存可以被当成垃圾回收,不用手动释放。

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

DataView

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

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

必须在对已有的 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 
// 构造函数接收一个可选的字节偏移量和字节长度
// byteOffset=0 表示视图从缓冲起点开始
// byteLength=8 限制视图为前 8 个字节
const firstHalfDataView = new DataView(buf, 0, 8); 
alert(firstHalfDataView.byteOffset); // 0 
alert(firstHalfDataView.byteLength); // 8 
alert(firstHalfDataView.buffer === buf); // true 
// 如果不指定,则 DataView 会使用剩余的缓冲
// byteOffset=8 表示视图从缓冲的第 9 个字节开始
// byteLength 未指定,默认为剩余缓冲
const secondHalfDataView = new DataView(buf, 8); 
alert(secondHalfDataView.byteOffset); // 8
alert(secondHalfDataView.byteLength); // 8 
alert(secondHalfDataView.buffer === buf); // true

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

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

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

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

1.ElementType

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

ECMAScript 6 支持 8 种不同的 ElementType:

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

// 在内存中分配两个字节并声明一个 DataView 
const buf = new ArrayBuffer(2); 
const view = new DataView(buf); 
// 说明整个缓冲确实所有二进制位都是 0 
// 检查第一个和第二个字符
alert(view.getInt8(0)); // 0 
alert(view.getInt8(1)); // 0 
// 检查整个缓冲
alert(view.getInt16(0)); // 0 
// 将整个缓冲都设置为 1 
// 255 的二进制表示是 11111111(2^8 - 1)
view.setUint8(0, 255); 
// DataView 会自动将数据转换为特定的 ElementType 
// 255 的十六进制表示是 0xFF 
view.setUint8(1, 0xFF); 
// 现在,缓冲里都是 1 了
// 如果把它当成二补数的有符号整数,则应该是-1 
alert(view.getInt16(0)); // -1

2.字节序

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

DataView只支持两种约定:

  • 大端字节序:最高有效位保存在第一个字节,而最低有效位保存在最后一个字节

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

JavaScript 运行时所在系统的原生字节序决定了如何读取或写入字节,但 DataView 并不遵守这个约定。对一段内存而言,DataView 是一个中立接口,它会遵循你指定的字节序。DataView 的所有 API 方法都以大端字节序作为默认值,但接收一个可选的布尔值参数,设置为 true 即可启用小端字节序

// 在内存中分配两个字节并声明一个 DataView 
const buf = new ArrayBuffer(2); 
const view = new DataView(buf); 
// 填充缓冲,让第一位和最后一位都是 1 
view.setUint8(0, 0x80); // 设置最左边的位等于 1 
view.setUint8(1, 0x01); // 设置最右边的位等于 1 
// 缓冲内容(为方便阅读,人为加了空格)
// 0x8 0x0 0x0 0x1 
// 1000 0000 0000 0001 
// 按大端字节序读取 Uint16 
// 0x80 是高字节,0x01 是低字节
// 0x8001 = 2^15 + 2^0 = 32768 + 1 = 32769 
alert(view.getUint16(0)); // 32769 
// 按小端字节序读取 Uint16 
// 0x01 是高字节,0x80 是低字节
// 0x0180 = 2^8 + 2^7 = 256 + 128 = 384 
alert(view.getUint16(0, true)); // 384 
// 按大端字节序写入 Uint16 
view.setUint16(0, 0x0004); 
// 缓冲内容(为方便阅读,人为加了空格)
// 0x0 0x0 0x0 0x4 
// 0000 0000 0000 0100 
alert(view.getUint8(0)); // 0 
alert(view.getUint8(1)); // 4 
// 按小端字节序写入 Uint16 
view.setUint16(0, 0x0002, true); 
// 缓冲内容(为方便阅读,人为加了空格)
// 0x0 0x2 0x0 0x0 
// 0000 0010 0000 0000 
alert(view.getUint8(0)); // 2 
alert(view.getUint8(1)); // 0

3.边界情形

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

const buf = new ArrayBuffer(6); 
const view = new DataView(buf); 
// 尝试读取部分超出缓冲范围的值
view.getInt32(4); 
// RangeError 
// 尝试读取超出缓冲范围的值
view.getInt32(8); 
// RangeError 
// 尝试读取超出缓冲范围的值
view.getInt32(-1); 
// RangeError 
// 尝试写入超出缓冲范围的值
view.setInt32(4, 123); 
// RangeError

对于getInt32()方法来说,函数语义是以传入的偏移量为起点,向后读取对应字节,并且ArrayBuffer()和getInt32()方法传入的参数单位都是字节,那么这里相当于,buf里面有6个字节的内存空间,但是getInt32(4)想访问以第四个字节为起点往后4字节的内存空间,(为什么是往后4字节?因为32位等于4字节)就超出了范围

判断缓冲是否超出范围的公式:buf内存空间总量 - 方法中传入的偏移量 + 方法对应位 / 8 < 0

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

const buf = new ArrayBuffer(1); 
const view = new DataView(buf); 
view.setInt8(0, 1.5); 
alert(view.getInt8(0)); // 1 
view.setInt8(0, [4]); 
alert(view.getInt8(0)); // 4 
view.setInt8(0, 'f'); 
alert(view.getInt8(0)); // 0 
view.setInt8(0, Symbol()); 
// TypeError

定型数组

概述:

  • 定型数组是另一种形式的 ArrayBuffer 视图

  • 与DataView接近,区别在于特定于一种ElementType 且遵循系统原生的字节序

  • 提供适用面更广的API和更高的性能

  • 目的是为了提升与WebGL 等原生库交换二进制数据的效率

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

// 创建一个 12 字节的缓冲
const buf = new ArrayBuffer(12); 
// 创建一个引用该缓冲的 Int32Array 
const ints = new Int32Array(buf); 
// 这个定型数组知道自己的每个元素需要 4 字节
// 因此长度为 3 
alert(ints.length); // 3
// 创建一个长度为 6 的 Int32Array 
const ints2 = new Int32Array(6); 
// 每个数值使用 4 字节,因此 ArrayBuffer 是 24 字节
alert(ints2.length); // 6 
// 类似 DataView,定型数组也有一个指向关联缓冲的引用
alert(ints2.buffer.byteLength); // 24 
// 创建一个包含[2, 4, 6, 8]的 Int32Array 
const ints3 = new Int32Array([2, 4, 6, 8]); 
alert(ints3.length); // 4 
alert(ints3.buffer.byteLength); // 16 
alert(ints3[2]); // 6 
// 通过复制 ints3 的值创建一个 Int16Array 
const ints4 = new Int16Array(ints3); 
// 这个新类型数组会分配自己的缓冲
// 对应索引的每个值会相应地转换为新格式
alert(ints4.length); // 4 
alert(ints4.buffer.byteLength); // 8 
alert(ints4[2]); // 6 
// 基于普通数组来创建一个 Int16Array 
const ints5 = Int16Array.from([3, 5, 7, 9]); 
alert(ints5.length); // 4 
alert(ints5.buffer.byteLength); // 8 
alert(ints5[2]); // 7 
// 基于传入的参数创建一个 Float32Array 
const floats = Float32Array.of(3.14, 2.718, 1.618); 
alert(floats.length); // 3 
alert(floats.buffer.byteLength); // 12 
alert(floats[2]); // 1.6180000305175781 
定型数组的构造函数和实例都有一个 BYTES_PER_ELEMENT 属性,返回该类型数组中每个元素的大小:
alert(Int16Array.BYTES_PER_ELEMENT); // 2 
alert(Int32Array.BYTES_PER_ELEMENT); // 4 
const ints = new Int32Array(1), 
 floats = new Float64Array(1); 
alert(ints.BYTES_PER_ELEMENT); // 4 
alert(floats.BYTES_PER_ELEMENT); // 8 
如果定型数组没有用任何值初始化,则其关联的缓冲会以 0 填充:
const ints = new Int32Array(4); 
alert(ints[0]); // 0 
alert(ints[1]); // 0 
alert(ints[2]); // 0 
alert(ints[3]); // 0

1.定型数组行为

从很多方面看,定型数组和普通数组很相似。

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

  • []

  • copyWithin()

  • entries()

  • every()

  • fill()

  • filter()

  • find()

  • findIndex()

  • forEach()

  • indexOf()

  • join()

  • keys()

  • lastIndexOf()

  • length

  • map()

  • reduce()

  • reduceRight()

  • reverse()

  • slice()

  • some()

  • sort()

  • toLocaleString()

  • toString()

  • values()

其中,返回新数组的方法也会返回包含同样元素类型(element type)的新定型数组:

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

2.合并、赋值、修改定型数组

定型数组同样使用数组缓冲来存储数据,而数组缓冲无法调整大小

因此下列方法不适用于定型数组

  • concat()

  • push()

  • shift()

  • unshift()

  • pop()

  • splice()

不过,定型数组也提供了两个新方法,可以快速向外或向内复制数据:

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

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

// 第一个参数是应该返回的数组类型 
// 其余参数是应该拼接在一起的定型数组
function typedArrayConcat(typedArrayConstructor, ...typedArrays) { 
 // 计算所有数组中包含的元素总数
 const numElements = typedArrays.reduce((x,y) => (x.length || x) + y.length); 
 // 按照提供的类型创建一个数组,为所有元素留出空间
 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

3.上溢和下溢

什么是上溢和下溢:通俗来说,对于一个定型数组,如果存入的值大于其所能表示的最大正数时,称为上溢,反之称为下溢

例如,如果我们使用 16 位整数,其范围为 -32768 到 +32767。当一个数值超过 +32767 时,就会发生上溢。

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

定型数组对于可以存储的每个索引只接受一个相关位,而不考虑它们对实际数值的影响

对于上溢和下溢的处理:

// 长度为 2 的有符号整数数组
// 每个索引保存一个二补数形式的有符号整数
// 范围是-128(-1 * 2^7)~127(2^7 - 1)
const ints = new Int8Array(2); 
// 长度为 2 的无符号整数数组
// 每个索引保存一个无符号整数
// 范围是 0~255(2^7 - 1)
const unsignedInts = new Uint8Array(2); 
// 上溢的位不会影响相邻索引
// 索引只取最低有效位上的 8 位
unsignedInts[1] = 256; // 0x100 
console.log(unsignedInts); // [0, 0] 
unsignedInts[1] = 511; // 0x1FF 
console.log(unsignedInts); // [0, 255] 
// 下溢的位会被转换为其无符号的等价值
// 0xFF 是以二补数形式表示的-1(截取到 8 位), 
// 但 255 是一个无符号整数
unsignedInts[1] = -1 // 0xFF (truncated to 8 bits) 
console.log(unsignedInts); // [0, 255] 
// 上溢自动变成二补数形式
// 0x80 是无符号整数的 128,是二补数形式的-128 
ints[1] = 128; // 0x80 
console.log(ints); // [0, -128] 
// 下溢自动变成二补数形式
// 0xFF 是无符号整数的 255,是二补数形式的-1 
ints[1] = 255; // 0xFF 
console.log(ints); // [0, -1]

除了 8 种元素类型,还有一种“夹板”数组类型:Uint8ClampedArray,不允许任何方向溢出。

超出最大值 255 的值会被向下舍入为 255,而小于最小值 0 的值会被向上舍入为 0。

const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256]); 
console.log(clampedInts); // [0, 0, 255, 255]

Brendan Eich:“Uint8ClampedArray 完全是 HTML5canvas 元素的历史留存。除非真的做跟 canvas 相关的开发,否则不要使用它。

Map

在ES6之前,存储键值对数据使用的是Object,但是这种实现是存在问题的,因此Map作为ES6的新特性,一种新的集合类型,用来专门存储键值对

基本API

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

const map = new Map();

如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含键/值对数组。可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中

// 使用嵌套数组初始化映射
const m1 = new Map([ 
 ["key1", "val1"], 
 ["key2", "val2"], 
 ["key3", "val3"] 
]); 
alert(m1.size); // 3 
// 使用自定义迭代器初始化映射
const m2 = new Map({ 
 [Symbol.iterator]: function*() { 
 yield ["key1", "val1"]; 
 yield ["key2", "val2"]; 
 yield ["key3", "val3"]; 
 } 
}); 
alert(m2.size); // 3 
// 映射期待的键/值对,无论是否提供
const m3 = new Map([[]]); 
alert(m3.has(undefined)); // true 
alert(m3.get(undefined)); // undefined

Map实例的操作方法:

  • set(key,value):新增键值对

  • get(key):传入一个键,如果有对应的映射值,则返回映射值

  • has(key):传入一个键,如果有对应的映射值,则返回true,否则返回false

  • delete(key):删除指定的映射

  • clear():删除所有的映射

const m = new Map(); 
alert(m.has("firstName")); // false 
alert(m.get("firstName")); // undefined 
alert(m.size); // 0 
m.set("firstName", "Matt") 
 .set("lastName", "Frisbie"); 
alert(m.has("firstName")); // true 
alert(m.get("firstName")); // Matt 
alert(m.size); // 2 
m.delete("firstName"); // 只删除这一个键/值对
alert(m.has("firstName")); // false 
alert(m.has("lastName")); // true 
alert(m.size); // 1 
m.clear(); // 清除这个映射实例中的所有键/值对
alert(m.has("firstName")); // false 
alert(m.has("lastName")); // false 
alert(m.size); // 0

Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。Map 内部使用 SameValueZero 比较操作(ECMAScript 规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。与 Object 类似,映射的值是没有限制的

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"); 
alert(m.get(functionKey)); // functionValue 
alert(m.get(symbolKey)); // symbolValue 
alert(m.get(objectKey)); // objectValue 
// SameValueZero 比较意味着独立实例不冲突
alert(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"]

SameValue比较也可能导致意想不到的冲突

const m = new Map(); 
const a = 0/"", // NaN 
 b = 0/"", // NaN 
 pz = +0, 
 nz = -0;
alert(a === b); // false 
alert(pz === nz); // true 
m.set(a, "foo"); 
m.set(pz, "bar"); 
alert(m.get(b)); // foo 
alert(m.get(nz)); // 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); 
} 
// [key1,val1] 
// [key2,val2] 
// [key3,val3]

因为entries()是默认迭代器,因此可以直接使用Map实例进行迭代操作

for(let pair of m){
	console.log(pair);
}

如果不适用迭代器,而是使用回调方式,则可以调用映射的forEach(callback, opt_thisArg)方法,他接受两个参数:

  • callback:回调函数,依次迭代每个键值对

  • opt_thisArg:用于重写回调函数内部的this值

const m = new Map([ 
 ["key1", "val1"], 
 ["key2", "val2"], 
 ["key3", "val3"] 
]); 
m.forEach((val, key) => alert(`${key} -> ${val}`)); 
// key1 -> val1 
// key2 -> val2 
// key3 -> val3

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

const m = new Map([ 
 ["key1", "val1"], 
 ["key2", "val2"], 
 ["key3", "val3"] 
]); 
for (let key of m.keys()) { 
 alert(key); 
} 
// key1 
// key2 
// key3 
for (let key of m.values()) { 
 alert(key); 
} 
// value1 
// value2 
// value3

键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。当然,这并不妨碍修改作为键或值的对象内部的属性,因为这样并不影响它们在映射实例中的身份:

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

对于多数任务来说,Object和Map只是个人偏好问题,影响不大,但是对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别

1.内存占用

Object和Map在不同浏览器上的实现时不同的。虽然不同浏览器情况不同,但是给定固定大小的内存,Map大约可以比Object多存储50%的键值对

2.插入性能

对于这两个类型来说,插入速度并不会随着键值对的数量而线性增加。如果代码设计大量插入操作,那么显然Map的性能更佳

3.查找速度

对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况(比如只包含少量键/值对)下可能选择 Object 更好一些。

4.删除性能

使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。

为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null。但很多时候,这都是一种讨厌的或不适宜的折中。

而对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择 Map

WeakMap

ES6新增的"弱映射"(WeakMap)是一种新的集合类型

为这门语言带来了增强的键值对存储机制

WeakMap是Map的兄弟类型,其API也是Map的子集

WeakMap中的"弱",描述的是js垃圾回收程序对待"弱映射"中键的方式

基本API

可以使用 new 关键字实例化一个空的 WeakMap

const wm = new WeakMap();

弱映射中的键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出TypeError。值的类型没有限制

const key1 = {id: 1}, 
 key2 = {id: 2},
 key3 = {id: 3}; 
// 使用嵌套数组初始化弱映射
const wm1 = new WeakMap([ 
 [key1, "val1"], 
 [key2, "val2"], 
 [key3, "val3"] 
]); 
alert(wm1.get(key1)); // val1 
alert(wm1.get(key2)); // val2 
alert(wm1.get(key3)); // val3 
// 初始化是全有或全无的操作
// 只要有一个键无效就会抛出错误,导致整个初始化失败
const wm2 = new WeakMap([ 
 [key1, "val1"], 
 ["BADKEY", "val2"], 
 [key3, "val3"] 
]); 
// TypeError: Invalid value used as WeakMap key 
typeof wm2; 
// ReferenceError: wm2 is not defined 
// 原始值可以先包装成对象再用作键
const stringKey = new String("key1"); 
const wm3 = new WeakMap([ 
 stringKey, "val1" 
]); 
alert(wm3.get(stringKey)); // "val1"

初始化之后可以使用 set()再添加键/值对,可以使用 get()和 has()查询,还可以使用 delete()删除:

const wm = new WeakMap(); 
const key1 = {id: 1}, 
 key2 = {id: 2}; 
alert(wm.has(key1)); // false 
alert(wm.get(key1)); // undefined 
wm.set(key1, "Matt") 
 .set(key2, "Frisbie"); 
alert(wm.has(key1)); // true 
alert(wm.get(key1)); // Matt 
wm.delete(key1); // 只删除这一个键/值对
alert(wm.has(key1)); // false 
alert(wm.has(key2)); // true

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"); 
alert(wm.get(key1)); // val1 
alert(wm.get(key2)); // val2 
alert(wm.get(key3)); // val3

弱键

WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。

const wm = new WeakMap();
wm.set({},"val");

set()方法初始化了一个新对象并将它作为一个字符串的键。因为没有指向这个对象的其他引用,所以当这行代码执行完毕后,这个对象键就被当作垃圾回收了。然后这个键值对就从弱映射中消失了,使其作为一个空映射。在这个例子中,因为值也没有被引用,所以当映射被破坏后,值本身也会成为垃圾回收的目标

const wm = new WeakMap(); 
const container = { 
 key: {} 
}; 
wm.set(container.key, "val"); 
function removeReference() { 
 container.key = null; 
}

这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉

不可迭代键

因为WeakMap中的键值对在任何时候都可能被销毁,所以没必要提供迭代键值对的能力,因此WeakMap没有keys()、values()等方法,也没有必要一次性销毁所有键值对,因此也没有clear()方法

使用弱映射的场景

1.私有变量

弱映射造就了在 JavaScript 中实现真正私有变量的一种新方式。前提很明确:私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值

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); 
alert(user.getId()); // 123 
user.setId(456); 
alert(user.getId()); // 456 
// 并不是真正私有的
alert(wm.get(user)[user.idProperty]); // 456

对于上面的实现,外部代码只需要拿到对象实例的引用和弱映射,就可以取得“私有”变量了。为了避免这种访问,可以用一个闭包把 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); 
alert(user.getId()); // 123 
user.setId(456); 
alert(user.getId()); // 456

这样,拿不到弱映射中的健,也就无法取得弱映射中对应的值。虽然这防止了前面提到的访问,但整个代码也完全陷入了 ES6 之前的闭包私有变量模式。

2.节点元数据

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

什么是关联元数据:在编程中,关联元数据用于存储对象或值的一些额外信息,比如标记、状态、配置等。关联元数据可以是任何形式的信息,例如对象的创建时间、所有者、权限、缓存标记等

为什么WeakMap会适合用来存储节点的元数据,假设使用Map来存储节点元数据:

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

当loginButton 节点在DOM树的变化过程中被删除时,Map中还存储着loginButton 节点的元数据并不会被垃圾回收,显然这一过程是不合理的

然而如果使用WeakMap来存储元数据,由于弱映射的特性,当loginButton 节点被删除时,弱键占用的内存也会被垃圾回收机制回收掉

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

Set

ECMAScript 6 新增的 Set 是一种新集合类型,为这门语言带来集合数据结构。Set 在很多方面都像是加强的 Map,这是因为它们的大多数 API 和行为都是共有的

基本API

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

const m = new Set();

如果想在创建的同时初始化实例,则可以给 Set 构造函数传入一个可迭代对象,其中需要包含插入到新集合实例中的元素

// 使用数组初始化集合 
const s1 = new Set(["val1", "val2", "val3"]); 
alert(s1.size); // 3 
// 使用自定义迭代器初始化集合
const s2 = new Set({ 
 [Symbol.iterator]: function*() { 
 yield "val1"; 
 yield "val2"; 
 yield "val3"; 
 } 
}); 
alert(s2.size); // 3

常用方法:

  • add(p):用于给创建的Set实例增加值,返回集合的实例,可以用多个add实现添加操作

  • has(p):用于查询Set实例中是否包含传入值

  • size:用于查询Set的长度

  • delete(p):删除特定元素

  • clear():删除所有元素

const s = new Set(); 
alert(s.has("Matt")); // false 
alert(s.size); // 0 
s.add("Matt") 
 .add("Frisbie"); 
alert(s.has("Matt")); // true 
alert(s.size); // 2 
s.delete("Matt"); 
alert(s.has("Matt")); // false 
alert(s.has("Frisbie")); // true 
alert(s.size); // 1 
s.clear(); // 销毁集合实例中的所有值
alert(s.has("Matt")); // false 
alert(s.has("Frisbie")); // false 
alert(s.size); // 0

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); 
alert(s.has(functionVal)); // true 
alert(s.has(symbolVal)); // true 
alert(s.has(objectVal)); // true 
// SameValueZero 检查意味着独立的实例不会冲突
alert(s.has(function() {})); // false

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

const s = new Set(); 
const objVal = {}, 
 arrVal = []; 
s.add(objVal); 
s.add(arrVal); 
objVal.bar = "bar"; 
arrVal.push("bar"); 
alert(s.has(objVal)); // true 
alert(s.has(arrVal)); // true

add()和 delete()操作是幂等的。delete()返回一个布尔值,表示集合中是否存在要删除的值

幂等:在计算机科学中,"幂等"(Idempotence)是指一个操作被重复执行多次所产生的结果与仅执行一次的结果相同

const s = new Set(); 
s.add('foo'); 
alert(s.size); // 1 
s.add('foo'); 
alert(s.size); // 1 
// 集合里有这个值
alert(s.delete('foo')); // true 
// 集合里没有这个值
alert(s.delete('foo')); // false

顺序于迭代

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

集合实例可以提供一个迭代器(Iterator),能以插入顺序生成集合内容

获取迭代器的方式:

  • values()方法或其别名方法keys(),与Map略有不同

  • 使用Symbol.iterator 属性,它引用values()方法

const s = new Set(["val1", "val2", "val3"]); 
alert(s.values === s[Symbol.iterator]); // true 
alert(s.keys === s[Symbol.iterator]); // true 
for (let value of s.values()) { 
 alert(value); 
} 
// val1 
// val2 
// val3 
for (let value of s[Symbol.iterator]()) { 
 alert(value); 
} 
// val1 
// val2 
// val3

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

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

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

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

Set提供forEach()方法以回调的方式替代迭代器的方式依次迭代集合中的每个键值对。传入的回调接收可选的第二个和参数,用于重写回调的内部的this值

const s = new Set(["val1", "val2", "val3"]); 
s.forEach((val, dupVal) => alert(`${val} -> ${dupVal}`)); 
// val1 -> val1 
// val2 -> val2 
// val3 -> val3

修改集合中值的属性不会影响其作为集合值的身份:

const s1 = new Set(["val1"]); 
// 字符串原始值作为值不会被修改
for (let value of s1.values()) {
value = "newVal"; 
 alert(value); // newVal 
 alert(s1.has("val1")); // true 
} 
const valObj = {id: 1}; 
const s2 = new Set([valObj]); 
// 修改值对象的属性,但对象仍然存在于集合中
for (let value of s2.values()) { 
 value.id = "newVal"; 
 alert(value); // {id: "newVal"} 
 alert(s2.has(valObj)); // true 
} 
alert(valObj); // {id: "newVal"}

从各方面来看,Set 跟 Map 都很相似,只是 API 稍有调整。唯一需要强调的就是集合的 API 对自身的简单操作。很多开发者都喜欢使用 Set 操作,但需要手动实现:或者是子类化 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);
  }
  // 返回两个或更多集合的并集
  static union(a, ...bSets) {
    const unionSet = new XSet(a);
    for (const b of bSets) {
      for (const bValue of b) {
        unionSet.add(bValue);
      }
    }
    return unionSet;
  }
  // 返回两个或更多集合的交集
  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;
  }
  // 返回两个集合的差集
  static difference(a, b) {
    const differenceSet = new XSet(a);
    for (const bValue of b) {
      if (a.has(bValue)) {
        differenceSet.delete(bValue);
      }
    }
    return differenceSet;
  }
  // 返回两个集合的对称差集
  static symmetricDifference(a, b) {
    // 按照定义,对称差集可以表达为
    return a.union(b).difference(a.intersection(b));
  }
  // 返回两个集合(数组对形式)的笛卡儿积
  // 必须返回数组集合,因为笛卡儿积可能包含相同值的对
  static cartesianProduct(a, b) {
    const cartesianProductSet = new XSet();
    for (const aValue of a) {
      for (const bValue of b) {
        cartesianProductSet.add([aValue, bValue]);
      }
    }
    return cartesianProductSet;
  }
  // 返回一个集合的幂集
  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提供的新的数据类型,和Set是兄弟类型,API是Set的API的子集。WeakSet中描述的弱是指JavaScript垃圾回收程序对待"弱集合"中值的处理方式

基本API

用new关键字实例化一个WeakSet

const ws = new WeakSet();

弱集合中的值,只能是Object或者继承自Object的类型,尝试使用非对象设置值会抛出TypeError

在初始化时填充对象,要提供一个可迭代的对象,其中要包含。可迭代对象会按照顺序插入到新的实例中

const val1 = {id: 1}, 
 val2 = {id: 2}, 
 val3 = {id: 3}; 
// 使用数组初始化弱集合
const ws1 = new WeakSet([val1, val2, val3]); 
alert(ws1.has(val1)); // true 
alert(ws1.has(val2)); // true 
alert(ws1.has(val3)); // true 
// 初始化是全有或全无的操作
// 只要有一个值无效就会抛出错误,导致整个初始化失败
const ws2 = new WeakSet([val1, "BADVAL", val3]); 
// TypeError: Invalid value used in WeakSet 
typeof ws2; 
// ReferenceError: ws2 is not defined 
// 原始值可以先包装成对象再用作值
const stringVal = new String("val1"); 
const ws3 = new WeakSet([stringVal]); 
alert(ws3.has(stringVal)); // true

常用方法:

  • add(val):向实例中添加新的值val

  • has(val):查询弱集合中是否存在值val

  • delete(val):删除指定的值val

const ws = new WeakSet(); 
const val1 = {id: 1}, 
 val2 = {id: 2}; 
alert(ws.has(val1)); // false 
ws.add(val1).add(val2); 
alert(ws.has(val1)); // true 
alert(ws.has(val2)); // true 
ws.delete(val1); // 只删除这一个值
alert(ws.has(val1)); // false 
alert(ws.has(val2)); // true

add()方法返回的是弱集合的实例,因此多个add方法可以连用

const val1 = {id: 1}, 
 val2 = {id: 2}, 
 val3 = {id: 3}; 
const ws = new WeakSet().add(val1); 
ws.add(val2) 
 .add(val3); 
alert(ws.has(val1)); // true 
alert(ws.has(val2)); // true 
alert(ws.has(val3)); // true

弱值

WeakSet 中“weak”表示弱集合的值不属于正式的引用,不会阻止垃圾回收

const ws = new WeakSet(); 
ws.add({});

add()方法传入一个空对象,由于没有在其他地方被引用,因此在执行这两行代码时,方法中的空对象被垃圾回收机制回收了,最后实例ws还是一个空集合

const ws = new WeakSet(); 
const container = { 
 val: {} 
}; 
ws.add(container.val); 
function removeReference() { 
 container.val = null; 
}

在这个例子中,container维护着弱集合值的引用,因此实例ws不会是空集合,但是当调用removeReference()方法时,就会摧毁弱集合值的最后一个引用,因此,垃圾回收机制就会把这个值清理掉

不可迭代值

因为 WeakSet 中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力。因此弱集合没有迭代方法values()、entries()和keys()方法,也没有全部删除的方法clear()

WeakSet 之所以限制只能用对象作为值,是为了保证只有通过值对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了(暂时不太理解)

使用弱集合

相比于 WeakMap 实例,WeakSet 实例的用处没有那么大。不过,弱集合在给对象打标签时还是有价值的。

普通Set:

const disabledElements = new Set(); 
const loginButton = document.querySelector('#login'); 
// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton);

这里的问题是,当dom节点在某些地方被删除时,disabledElements 中任然会存储被删除节点的标签信息,理论上来说,disabledElements中存储的有关节点的信息也应该同dom节点的生命周期一致

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

如果使用的是弱集合,那么当dom节点被删除时,引用不存在后,disabledElements 中的dom节点信息也会被删除

迭代与扩展操作

ECMAScript 6 新增的迭代器和扩展操作符对集合引用类型特别有用。这些新特性让集合类型之间相互操作、复制和修改变得异常方便。

有 4 种原生集合类型定义了默认迭代器:

  • 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 = 'bar'; 
console.log(arr2[0]); // { foo: 'bar' }

上面的这些类型都支持多种构建方法,比如 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] 
// 把数组复制到映射
let map = new Map(arr1.map((x) => [x, 'val' + x])); 
console.log(map); // Map {1 => 'val 1', 2 => 'val 2', 3 => 'val 3'} 
// 把数组复制到集合
let set = new Set(typedArr2); 
console.log(set); // Set {1, 2, 3} 
// 把集合复制回数组
let arr2 = [...set]; 
console.log(arr2); // [1, 2, 3]

小结

JavaScript 中的对象是引用值,可以通过几种内置引用类型创建特定类型的对象

  • 引用类型与传统面向对象编程语言中的类相似,但实现不同。

  • Object 类型是一个基础类型,所有引用类型都从它继承了基本的行为。

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

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

  • Date 类型提供了关于日期和时间的信息,包括当前日期和时间以及计算。

  • RegExp 类型是 ECMAScript 支持的正则表达式的接口,提供了大多数基本正则表达式以及一些高级正则表达式的能力。

JavaScript 比较独特的一点是,函数其实是 Function 类型的实例,这意味着函数也是对象。由于函数是对象,因此也就具有能够增强自身行为的方法

因为原始值包装类型的存在,所以 JavaScript 中的原始值可以拥有类似对象的行为。有 3 种原始值包装类型:Boolean、Number 和 String。它们都具有如下特点:

  • 每种包装类型都映射到同名的原始类型

  • 在以读模式访问原始值时,后台会实例化一个原始值包装对象,通过这个对象可以操作数据

  • 涉及原始值的语句只要一执行完毕,包装对象就会立即销毁。

JavaScript 还有两个在一开始执行代码时就存在的内置对象:Global 和 Math。其中,Global 对象在大多数 ECMAScript 实现中无法直接访问。不过浏览器将 Global 实现为 window 对象,node环境将Global实现为global对象。所有全局变量和函数都是 Global 对象的属性。Math 对象包含辅助完成复杂数学计算的属性和方法。

ECMAScript 6 新增了一批引用类型:Map、WeakMap、Set 和 WeakSet。

0

评论区