JavaScript学习笔记(4):集合引用类型

数组

特性

创建数组

创建数组可以通过字面量或构造函数:

1
2
3
4
let arr = []; // 数组字面量
let arr = new Array(); // 创建空数组
let arr = new Array(5); // 创建有五个元素的数组,元素默认值undefined
let arr = new Array(1,2,3); // 创建数组[1,2,3]

其他特性

  • 数组中每个槽位可以存储任意类型的数据。
  • 使用数组字面量初始化数组时,可以使用一串逗号来创建空位。ES6新增方法普遍将这些空位当成存在的元素,只不过值为undefined。ES6之前的方法则会忽略这个空位,但具体的行为也会因方法而异。
  • 数组中元素的数量保存在length属性中,这个属性始终返回0或大于0的值。
  • 通过修改length属性,可以从数组末尾删除或添加元素。如果将length设置为大于数组元素数的值,则新添加的元素都将以undefined填充,因此可以通过下标直接扩充数组容量,中间没有值的数组元素的值为undefined。

方法

静态方法

  • Array.from():用于将类数组结构(拥有length属性和数值索引的对象)转换为数组实例。
  • Array.of():用于将一组参数转换为数组实例。
  • Array.isArray():判断一个对象是不是数组。如果网页里有多个框架,可能有多个全局上下文,也就可能有多个Array构造函数,instanceof基于原型的判断可能会失效,但该静态方法总能识别出数组对象。

有以下例子:

1
2
3
4
5
6
7
8
let arr1 = Array.from("string");
console.log(arr1); // ['s', 't', 'r', 'i', 'n', 'g']
let arr2 = Array.from({0:"Dasen", 1:22, length:2});
console.log(arr2); // ['Dasen', 22]
let arr3 = Array.of(1,2,3,4,5);
console.log(arr3); // [1, 2, 3, 4, 5]
console.log(Array.isArray(arr2)); // true
console.log(Array.isArray({0:"Dasen", 1:22, length:2})); // false

迭代器方法

  • keys():返回数组索引的迭代器。
  • values():返回数组元素的迭代器。for-of循环默认使用该迭代器迭代数组的值。
  • entries():返回索引/值对的迭代器。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = ["red", "blue", "yellow"];
let iter = arr.values();
console.log(iter.next()); // {value: 'red', done: false}
console.log(iter.next()); // {value: 'blue', done: false}
console.log(iter.next()); // {value: 'yellow', done: false}
console.log(iter.next()); // {value: undefined, done: true}
console.log(arr.keys()); // Array Iterator
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
// 0 red
// 1 blue
// 2 yellow

赋值和填充方法

  • copyWithin():会按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置,第一个参数是要复制到的索引位置,后两个可选参数指定了要赋值的区间,三个索引都支持负值。
  • fill():以向一个已有的数组中插入一些相同的值,第一个参数是这个值,后两个可选的参数指定了一个插入区间,不指定则视为全部区间,区间可以使用负值指定。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// copyWithin
let arr = [1,2,3,4,5,6,7,8,9];
arr.copyWithin(3);
console.log(arr); // [1, 2, 3, 1, 2, 3, 4, 5, 6]
arr = [1,2,3,4,5,6,7,8,9];
arr.copyWithin(2,6);
console.log(arr); // [1, 2, 7, 8, 9, 6, 7, 8, 9]
arr = [1,2,3,4,5,6,7,8,9];
arr.copyWithin(2,6,8);
console.log(arr); // [1, 2, 7, 8, 5, 6, 7, 8, 9]
arr = [1,2,3,4,5,6,7,8,9];
arr.copyWithin(-7,-3,-1);
console.log(arr); // [1, 2, 7, 8, 5, 6, 7, 8, 9]
// fill
arr.fill(0,5,8);
console.log(arr); // [1, 2, 7, 8, 5, 0, 0, 0, 9]
arr.fill(0,5);
console.log(arr); // [1, 2, 7, 8, 5, 0, 0, 0, 0]
arr.fill(0);
console.log(arr); // [0, 0, 0, 0, 0, 0, 0, 0, 0]

转换方法

  • valueOf():返回数组本身。
  • toString():返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串。
  • join():返回一个字符串,接收一个参数,作为拼接每个数组元素的分隔符。

有以下例子:

1
2
3
4
let arr = [1,2,3,4,5,6,7,8,9];
console.log(arr.toString()); // 1,2,3,4,5,6,7,8,9
console.log(arr.join(",")); // 1,2,3,4,5,6,7,8,9
console.log(arr.join(" ")); // 1 2 3 4 5 6 7 8 9

栈和队列方法

  • push():从末尾插入元素。
  • pop():从末尾删除元素并返回删除的元素。
  • shift():从开头删除元素并返回删除的元素。
  • unshift():从开头插入元素。

排序方法

  • reverse():将数组反向存储。原地操作数组。
  • sort():排序数组。原地操作数组。默认情况下升序排列,可以接收一个比较函数作为参数,这个函数接收两个参数,分别是数组一前一后的两个值,不需要交换它们的顺序时函数返回负值,需要交换时返回正值,两元素相等时返回0。

有以下例子:

1
2
3
4
5
6
7
let arr = ["1", "2", "11", "12"];
arr.reverse();
console.log(arr); // ['12', '11', '2', '1']
arr.sort();
console.log(arr); // ['1', '11', '12', '2']
arr.sort((a,b) => parseInt(a)-parseInt(b));
console.log(arr); // ['1', '2', '11', '12']

搜索和位置方法

  • indexOf():传入要查找的元素,第二个参数可选,是开始查找的位置,返回找到的索引,找不到返回-1。
  • lastIndexOf():同上,区别是反向查找。
  • includes():返回布尔值,表示数组中是否包含这样一个元素。
  • find():接收一个函数作为参数,使用这个函数对数组每一项进行迭代,函数收到的第一个参数是当前迭代的数组项,第二个参数是当前项索引,第三个参数是数组本身,由这个函数是否返回真值来判断当前项是不是要找的元素。该方法返回找到的第一个元素。
  • findIndex():同上,区别是返回的是找到元素的索引。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let arr = [1, 2, 3, 4, 5, 4, 3, 2, 1];
console.log(arr.indexOf(3)); // 2
console.log(arr.lastIndexOf(3)); // 6
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
function find1(val) {
// 寻找第一个能被 3 整除的数
return !(val % 3);
}
function find2(val, index, array) {
// 寻找能被 3 整除并位于数组后半部分的数
if (val % 3) {
return false;
} else {
if (index >= (array.length - 1) / 2) {
return true;
} else {
return false;
}
}
}
console.log(arr.find(find1)); // 3
console.log(arr.find(find2)); // 6
console.log(arr.findIndex(find1)); // 2
console.log(arr.findIndex(find2)); // 5

迭代方法

  • every():接收两个参数,第一个参数是以数组的项为参数运行的函数,第二项是可选的上下文对象,作为函数运行时this的值。对每一项都运行这个函数,所有函数都返回true时函数返回true。
  • filter():参数同上,但会把函数返回true时的项组成数组返回,实现数组过滤。
  • forEach():参数同上,但没有返回值,只是对每一项运行函数。
  • map():参数同上,但函数返回的不一定是布尔值,将函数对每一项返回的结果按顺序组成数组返回。
  • some():参数同上,但对每一项都运行这个函数,只要有其中一个为true就返回true。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
let arr1 = [1, "a", 2, "b", 3, "c"];
let arr2 = [1, 2, 3, 4, 5, 6];
console.log(arr1.every((val) => typeof val === "number")); // false
console.log(arr2.every((val) => typeof val === "number")); // true
console.log(arr1.some((val) => typeof val === "string")); // true
console.log(arr2.some((val) => typeof val === "string")); // false
console.log(arr1.filter((val) => typeof val === "number")); // [1, 2, 3]
console.log(arr1.filter((val) => typeof val === "string")); // ['a', 'b', 'c']
let sum = 0;
arr2.forEach((val) => sum+=val)
console.log(sum); // 21
console.log(arr2.map((val) => val.toString())); // ['1', '2', '3', '4', '5', '6']

归并方法

  • reduce():接收两个参数。第一个参数为一个函数,这个函数接收四个参数:上一个归并值、当前项、当前项索引、数组本身。第二个可选的参数为归并的起点索引。对于没有规定起点的该方法,第一次迭代会将数组第一项作为函数的第一个参数,第二项作为当前项,之后会把上一次迭代结果返回值作为下一次迭代的第一个参数,依次往后迭代数组项,直到所有的数组项都被归并处理。
  • reduceRight():同上,但反向迭代。

使用归并方法可以对数组完成一些统计操作,如数组求和:

1
2
3
let arr = [1, 2, 3, 4, 5, 6];
let sum = arr.reduce((presum, cur) => presum+cur);
console.log(sum); // 21

还有统计2在数组中出现了几次:

1
2
3
4
5
6
7
8
9
10
11
12
let arr = [1, 2, 3, 2, 5, 6, 2, 8, 9];
let count = arr.reduce((precount, cur, index) => {
if(index === 1) {
let c = 0;
if(precount === 2) c++;
if(cur === 2) c++;
return c;
} else {
return precount + (cur === 2);
}
});
console.log(count); // 3

但是用来求斐波那契数列的第n项这样的奇思妙想是不能这样实现的,比如:

1
2
3
4
5
6
7
8
9
10
let arr = [0, 1];
let n = 10;
let res = arr.reduce((pre, cur, index, array) => {
if(index <= n) {
array.push(pre+cur);
}
return cur;
});
console.log(res); // 1
console.log(arr); // [0, 1, 1]

原因是对数组执行该方法时,会建立数组的一个副本,迭代数组的项是迭代的这个副本数组,往原数组上面push元素是影响不到迭代次数的。这样防止了无线重写数组可能造成的死循环。

其他方法

(1)concat()方法:数组拼接。该方法接受若干个参数,对于某个参数,如果是数组,就把该数组中的元素依次拼接在原数组后面,如果不是数组,就作为一个数组项加入在数组后面,最后返回一个拼接后的数组。注意:该方法不会改变原数组,操作在副本上进行。

有以下例子:

1
2
3
4
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为false就可以阻止concat方法把它拆开了:

1
2
3
4
5
6
let colors = ["red", "green", "blue"];
let colors2 = ["black", "brown"];
let colors3 = ["black", "brown"];
colors3[Symbol.isConcatSpreadable] = false;
console.log(colors.concat(colors2)); // ['red', 'green', 'blue', 'black', 'brown']
console.log(colors.concat(colors3)); // ['red', 'green', 'blue', Array(2)]

(2)slice()方法:从原数组中截取出一个子数组。接受两个参数作为一个区间,第二个参数可省略。返回得到的子数组。

有以下例子:

1
2
3
let arr = [1,2,3,4,5,6,7,8,9];
console.log(arr.slice(5)); // [6, 7, 8, 9]
console.log(arr.slice(5,7)); // [6, 7]

(3)splice()方法:该方法前两个参数分别是开始索引和区间长度,来确定一个区间,后面若干参数指定了要插入的元素。该方法实际上是区间替换,将指定的区间里的元素替换为给出的若干元素,那么就可以实现除了替换外的另外两个功能:

  • 删除区间:只传递一个区间,但是不给出要插入的元素,那么就只进行区间删除操作。
  • 插入元素:指定了开始索引后,区间长度为0,则意味着不删除区间,只在索引位置插入给出的元素。

该方法会操作原数组,返回值是从原数组中移除的元素构成的数组,如果没移除元素返回的就是空数组。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
let arr = [1,2,3,4,5,6,7,8,9];
let returnVal;
returnVal = arr.splice(5,4,4,3,2,1); // 把 5 6 7 8 替换为 4 3 2 1
console.log(arr); // [1, 2, 3, 4, 5, 4, 3, 2, 1]
console.log(returnVal); // [6, 7, 8, 9]
returnVal = arr.splice(4,arr.length-4); // 只保留前四个元素,删除后面所有元素
console.log(arr); // [1, 2, 3, 4]
console.log(returnVal); // [5, 4, 3, 2, 1]
returnVal = arr.splice(0,0,4,3,2); // 在索引0位置上插入 4 3 2
console.log(arr); // [4, 3, 2, 1, 2, 3, 4]
console.log(returnVal); // []

映射

特性

创建映射

映射没有字面量表示,创建映射需要使用构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建一个空映射
let m = new Map();
// 创建一个映射,由键值对的数组给出初始值
let m2 = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
// 创建一个映射,由可迭代对象创建
let m = new Map({
* [Symbol.iterator]() {
yield ["key1", "val1"];
yield ["key2", "val2"];
yield ["key3", "val3"];
}
});

其他特性

  • 与Object只能使用数值、字符串或符号作为键不同,Map可以使用任何JavaScript数据类型作为键。
  • Map实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。
  • 键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。
  • 要获得映射包含多少个键值对,使用size属性而不是length。

使用映射还是对象

映射和对象都能满足键值访问的需求,对于多数Web开发任务来说,选择Object还是Map只是个人偏好问题,影响不大。但它们在内存和性能上还是存在显著差别的。

(1)内存占用:Object和Map的具体实现在不同浏览器间存在明显差异,但存储单个键值对所占用的内存都会随键的数量线性增加。虽然不同浏览器的情况不同,但大多情况下给定固定大小的内存,Map可以比Object大约多存储50%的键值对。

(2)插入性能:向Object和Map中插入新键值对的消耗大致相当,插入速度并不会随着键值对数量而线性增加,但插入Map在所有浏览器中一般会稍微快一点儿。如果代码涉及大量插入操作,那么使用Map。

(3)查找速度:对这两个类型而言,查找速度也不会随着键值对数量增加而线性增加。在数据量大的时候从Object和Map中查找键值对的性能差异几乎可以忽略不计,用哪个都差不多,但如果只包含少量键值对,则Object有时候速度更快。此外如果Object使用连续整数作为属性时,行为和数组类似,那么浏览器引擎可以对它进行优化,在内存中使用更高效的布局,这对Map来说是不可能的。

(4)删除性能:使用delete删除Object属性的性能很差,甚至还因此出现了一些伪删除对象属性的操作,比如把属性值设置为undefined或null。而对大多数浏览器引擎来说,Map的delete()操作都非常快。如果代码涉及大量删除操作,那么选择Map。

方法

键值方法

  • set():添加键值对,两个参数分别是键和值。set方法返回原映射实例,因此可以连缀使用set。
  • get():根据键读值。
  • has():判断键是否存在,返回布尔值。
  • delete():根据键删除键值对。
  • clear():清空实例中所有键值对。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
let m = new Map();
console.log(m); // Map(0)
m.set("name","Dasen").set("age",22); // 连缀使用set
console.log(m); // Map(2) {name => Dasen, age => 22}
console.log(m.get("name")); // Dasen
console.log(m.has("age")); // true
m.delete("age");
console.log(m.has("age")); // false
console.log(m.size); // 1
m.clear();
console.log(m); // Map(0)

迭代器方法

  • values():返回一个值的迭代器。
  • keys():返回一个键的迭代器。
  • entries():返回一个键值对迭代器,迭代器每次返回一个键值对。for-of默认使用的迭代器就是它。
  • forEach():第一个参数为一个回调函数,接收两个参数分别为值和键(注意值在前,键在后,为的是只迭代值时可以省略第二个参数),第二个参数为一个对象以重写回调函数内部的this值。该方法对每个键值对运行回调函数进行一些操作。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
let valIter = m.values();
console.log(valIter.next()); // {value: 'val1', done: false}
console.log(valIter.next()); // {value: 'val2', done: false}
console.log(valIter.next()); // {value: 'val3', done: false}
console.log(valIter.next()); // {value: undefined, done: true}
for (const key of m.keys()) {
console.log(key);
}
// key1
// key2
// key3
for (const [key, val] of m) {
console.log(key,":",val);
}
// key1 : val1
// key2 : val2
// key3 : val3
m.forEach((val,key) => console.log(key,":",val));
// key1 : val1
// key2 : val2
// key3 : val3

弱映射

特性

创建弱映射

弱映射的创建形式和映射一样,只能通过构造函数,并且也支持通过可迭代对象创建。

“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收,同时这些键也不可迭代的。
但弱映射中值的引用不是弱引用。只要键存在,键值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
弱映射中的键只能是Object或者继承自Object的类型,尝试使用非对象设置键会抛出TypeError。值的类型没有限制。

方法

弱映射支持的方法是映射的子集,它支持set()get()has()delete()四个方法,并且使用方式也和映射的对应方法一致。

应用

私有变量

把私有变量作为一个对象存储在一个弱映射中,以对象实例为键。这样当对象被销毁后弱映射中存储的私有变量也就会没了。但是其实这样还是挺鸡肋的,因为如果拿到了这个弱映射和实例对象,那么就可以在外部访问到私有变量了,要想解决这个问题,只能把它们都封装在一个闭包里,这就还是要使用ES6之前的闭包模式的私有变量。

有以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let User = (() => {
const wm = new WeakMap();
class User {
constructor(id) {
this.setId(id);
}
setPrivate(property, value) {
const privateProperty = wm.get(this) || {};
privateProperty[property] = value;
wm.set(this, privateProperty);
}
getPrivate(property) {
const privateProperty = wm.get(this) || {};
return privateProperty[property];
}
setId(id) {
this.setPrivate("id",id);
}
getId() {
return this.getPrivate("id");
}
}
return User;
})();
let user = new User(2021);
console.log(user.getId()); // 2021

存储DOM信息

如果要存储DOM的一些信息,那么用弱映射是一个好选择,因为如果使用普通映射,DOM会成为键,这就增加了无端的引用,如果把DOM从页面上移除,本该被回收的DOM对象不能正常被垃圾回收。而使用弱映射就不会这样的问题。

集合

特性

创建集合

集合也没有对应的字面量表示方式,也只能使用构造函数创建:

1
2
3
4
// 创建空集合
let s = Set();
// 从可迭代对象创建集合
let s = Set([1,2,3]);

其他特性

集合实际上就像是只有键、没有值的映射,它的特性和映射基本相同,如Set会维护值插入时的顺序,支持按顺序迭代,通过size属性访问项目的数量,可以包含任意类型的数据等特性。

Set中的元素不会有重复的,就像Map中的键不会重复一样。

方法

元素方法

  • add():向集合中添加一个元素,元素已存在则什么都不做。可以连缀操作。
  • has():查询元素是否在集合里。
  • delete():删除一个元素。
  • clear():清空集合内的元素。

迭代器方法

  • values():返回所有元素的迭代器。
  • keys():同上。
  • entries():返回包含所有“键值对”的迭代器,这里键值对其实就视为是键和值相同。
  • forEach():对每个元素使用回调函数进行迭代,回调函数接受两个参数分别是每个元素的键和值,当然这里键和值也视为相等的。

弱集合

特性

创建弱集合

弱集合的创建形式和集合一样,只能通过构造函数,并且也支持通过可迭代对象创建。

和弱映射的“弱”特性类似。

方法

弱集合支持的方法是集合的子集,它支持add()has()delete()三个方法,并且使用方式也和集合的对应方法一致。

应用

弱集合非常适合给DOM打标签。假如某些DOM元素需要有一个共同的标签,则只需要建立一个弱集合,把它们放进去就可以了。弱集合在这些DOM元素被移除页面时不会妨碍垃圾回收。




* 你好,我是大森。如果文章内容帮到了你,你可通过下方付款二维码支持作者 *