js学习笔记(6)——集合类型

Li Guangqiao - 12/08/2023

js

集合引用类型

Object

Object很适合存储和在应用程序间交换数据。

//声明方式
let obj = new Object();
obj.age = 1;
let obj_one = {
    age:1
};
//属性访问
//点语法
console.log(obj.age);
//变量语法
console.log(obj["age"]);

Object不管是实验和生产环境中都很常用所以这里不做过多的阐述

Array

Array同样是js中常用的引用类型。

js的数组不同于其他编程语言,其每一个槽位都可以存储任意类型的数据。

数组创建

//长度为20的空数组
let colors = new Array(20);
//成员为"1","2"的数组
colors = new Array("1","2");
colors = ["1","2"];

ES6中还有两个创建数组的方法

// 字符串会被拆分为单字符数组
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]

数组空位

生产环境数组空位会有诸多隐患故暂时不做过多的了解。

数组索引

常规的数组索引与其他语言保持一致,本小节重点将放在数组的length属性上。

数组检测

迭代器方法

ES6中Array的原型暴露了3个数组检索方法

const a = ["foo", "bar", "baz", "qux"]; 
// 因为这些方法都返回迭代器,所以可以将它们的内容
// 通过 Array.from()直接转换为数组实例
const aKeys = Array.from(a.keys()); 
const aValues = Array.from(a.values()); 
const aEntries = Array.from(a.entries()); 
console.log(aKeys); // [0, 1, 2, 3] 
console.log(aValues); // ["foo", "bar", "baz", "qux"] 
console.log(aEntries); // [[0, "foo"], [1, "bar"], [2, "baz"], [3, "qux"]]

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

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

复制和填充方法

ES6 新增了两个方法:批量复制方法 copyWithin(),以及填充数组方法 fill()。这两个方法的函数签名类似,都需要指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。使用这个方法不会改变数组的大小。

转换方法

所有对象都会有三个转换方法:

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

栈方法

LIFO(Last-In-First-Out)

队列方法

FIFO(First-In-First-Out)

排序方法

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

操作方法

搜索和位置方法

ES提供两类搜索数组方法:

迭代方法

ES为数组定义了5个迭代方法。每个方法接受两个参数:以每一项为参数运行的函数、可选的作为函数运行上下文的作用域对象(调整函数中的this指向)。传递每个方法的函数接受3个参数:数组元素、元素索引和数组本身。

归并方法

ES 为数组提供了两个归并方法:reduce()reduceRight()。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。reduce()方法从数组第一项开始遍历到最后一项。而 reduceRight()从最后一项开始遍历至第一项。

​ 这两个方法都接收两个参数:对每一项都会运行的归并函数,以及可选的以之为归并起点的初始值。传给 reduce()reduceRight()的函数接收 4 个参数:上一个归并值、当前项、当前项的索引和数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项。

​ 可以使用 reduce()函数执行累加数组中所有数值的操作,比如:

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

第一次执行归并函数时,prev 是 1,cur 是 2。第二次执行时,prev 是 3(1 + 2),cur 是 3(数组第三项)。如此递进,直到把所有项都遍历一次,最后返回归并结果。

reduceRight()方法与之类似,只是方向相反。来看下面的例子:

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

定型数组

目标

​ 定型数组(Typed Arrays)是为了解决在JavaScript中进行二进制数据处理时所遇到的一些问题而引入的。JavaScript原生的数组虽然非常强大,但在处理二进制数据方面存在一些限制和性能问题。这些问题包括:

  1. 数据类型问题: JavaScript中的普通数组是动态类型的,一个数组可以包含不同类型的数据。但在处理二进制数据时,需要确保数据类型的一致性,以避免解释错误的二进制值。
  2. 性能问题: 对于大量的二进制数据,JavaScript数组的处理效率相对较低。因为普通数组是动态的,包含了很多附加信息,而在处理二进制数据时,我们希望能够更紧凑地存储数据。
  3. 内存问题: 普通数组在存储数字时占用较多的内存,因为它们需要支持动态类型和其他功能。对于大型的二进制数据,这可能导致内存占用问题。

​ 定型数组通过引入固定数据类型和更紧凑的内存表示,解决了这些问题。它们提供了一种高性能的方式来操作和处理二进制数据,特别适用于需要进行低级别的二进制计算、图像处理、音视频编解码等任务。通过定型数组,开发人员可以更直接地访问和操作二进制数据,从而提高性能并减少内存占用。

注意:

  1. ArrayBuffer分配的内存不能超过Number.MAX_SAFE_INTEGER

    即 $$ 2^{53}-1 $$

  2. 声明ArrayBuffer则会将所有二进制位初始化 为0。

  3. 声明ArrayBuffer的堆内存可以被当成垃圾回收,不用手动释放。

​ 实际上我们无法直接对ArrayBuffer进行内容的读写操作。要读写必须通过视图进行操作。

视图

DataView

DataView 可以让你以各种不同的数据类型和字节顺序来读取和写入 ArrayBuffer 中的数据,从而更灵活地处理二进制数据。

DataView 适用于以下情况:

  1. 跨数据类型操作: 如果需要在同一个 ArrayBuffer 中以不同的数据类型进行读写操作,DataView 更适合,因为它可以手动指定数据类型。
  2. 精确控制字节顺序: DataView 允许你指定字节顺序,这在处理与不同机器端序(Big Endian 或 Little Endian)有关的数据时非常有用。
  3. 处理定制格式的二进制数据: 对于一些特殊格式的二进制数据,如网络通信协议、文件格式等,DataView 更容易进行灵活的解析和构建。

DataView的主要方法:

定型数组

JavaScript提供了以下几种类型的定型数组:

  1. Int8Array, Uint8Array, Uint8ClampedArray: 8位整数数组,分别表示带符号整数、无符号整数和用于图像处理的无符号整数(值范围在0-255之间)。

  2. Int16Array, Uint16Array: 16位整数数组,分别表示带符号整数和无符号整数。

  3. Int32Array, Uint32Array: 32位整数数组,分别表示带符号整数和无符号整数。

  4. Float32Array, Float64Array: 32位和64位浮点数数组,分别表示单精度和双精度浮点数。

  5. 如果定型数组没有用任何值初始化,则其关联的缓冲会以 0 填充:

    const ints = new Int32Array(4); 
    alert(ints[0]); // 0 
    alert(ints[1]); // 0 
    alert(ints[2]); // 0 
    alert(ints[3]); // 0
    

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

console.log(Int16Array.BYTES_PER_ELEMENT); // 2 
console.log(Int32Array.BYTES_PER_ELEMENT); // 4 
const ints = new Int32Array(1), 
floats = new Float64Array(1); 
console.log(ints.BYTES_PER_ELEMENT); // 4 
console.log(floats.BYTES_PER_ELEMENT); // 8

定型数组方法基本与普通数组一致。其中需要注意的是:

定型数组的简单例子

// 创建一个大小为 16 字节的 ArrayBuffer
const buffer = new ArrayBuffer(16);

// 创建一个 Int32Array 视图,表示整个 ArrayBuffer
const intArray = new Int32Array(buffer);

// 在 Int32Array 中设置值
intArray[0] = 42;
intArray[3] = 36;
console.log(intArray);//Int32Array(4) [42, 0, 0, 36, buffer: ArrayBuffer(16), byteLength: 16, byteOffset: 0, length: 4]


// 创建一个 Uint8Array 视图,表示 ArrayBuffer 中的一部分
const byteView = new Uint8Array(buffer); // 从第 8 个字节开始,长度为 4 字节

// 在 Uint8Array 中查看数据
console.log(byteView); // Uint8Array(16) [42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 0, 0, 0, buffer: ArrayBuffer(16), byteLength: 16, byteOffset: 0, length: 16]

Map

ES6新特性:键值对类型Map

  1. 创建映射Mapsize属性代表映射大小

    let m = new Map();
    // m = 1;
    let arr = [1,4,5,6];
    let m1 = new Map([
        ['a',1],
        ['b',2]
    ]);
    let m2 = new Map(arr.entries());
    console.log(m);
    console.log(m1);
    console.log(m2);
    //Map(0) {size: 0}
    //Map(2) {size: 2, a => 1, b => 2} 
    //Map(4) {size: 4, 0 => 1, 1 => 4, 2 => 5, 3 => 6}
    
    
    
  2. set方法

    注意:map类型的键、值可以是任意js数据类型,而object只能是数字、字符串和下划线

    m.set(m1,m2);
    let a = function(){
        console.log(1)
    };
    m.set(function(){
        console.log(1)
    },arr);
    m.set(a,arr);
    console.log(m);
    //Map(3) {size: 3, Map(2) {…} => Map(4) {…}, ƒ () => (4) [1, 4, 5, 6, …], ƒ () => (4) [1, 4, 5, 6, …]}
    
    
  3. get取值方法

    
    console.log(m.get(m1));
    console.log(m.get(function(){
        console.log(1)
    }));
    console.log(m.get(a));
    
    //Map(4) {size: 4, 0 => 1, 1 => 4, 2 => 5, 3 => 6}
    
    //undefined
    
    //(4) [1, 4, 5, 6]
    

    注意:键或者值的内部属性、属性值发生变化时,不会影响原本的映射关系例如:

    arr = [7,8,9];
    console.log(m.get(a));
    m1.delete('a');
    console.log(m1,m.get(m1));
    //(4) [1, 4, 5, 6]
    
    //Map(1) {size: 1, b => 2} Map(4) {size: 4, 0 => 1, 1 => 4, 2 => 5, 3 => 6}
    
  4. delete删除方法。传入对应的键,删除指定键值对。

  5. clear清空方法。清空map中所有的键值对。

    注意:Map 内部使用 SameValueZero 比较操作(ECMAScript 规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性,SameValueZero 比较也可能导致意想不到的冲突:

    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
    

顺序与迭代

Map实例与Object实例主要差异是:

Map实例会维护键值对插入顺序,因此可以根据插入顺序执行迭代操作。

Map同样拥有三个迭代器keysvaluesentries,顺序与插入顺序保持一致。

Map vs Object

  1. 内存占用:

    Object Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对

  2. 插入性能:

    ObjectMap中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然 Map 的性能更佳。

  3. 查找速度:

    与插入不同,从大型ObjectMap 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。

  4. 删除性能:

    使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefinednull。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Mapdelete()操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择 Map

综上所述:

大部分应用场景下可以优先使用Map类型,但作为长期支持的原始类型Object可以用于底层应用开发。

WeakMap

注意:弱映射中的键只能是Object或者继承自Object的子类(null除外)尝试使用非对象设置键会抛出TypeError。值的类型没有限制。

const key1 = {
    id:1
},key2 = {
    id:2
},key3 = {
    id:3
};
const wm = new WeakMap([
    [key1,{
        test_fst:1
    }],
    [key2,{
        test_sec:2
    }]
]);

console.log(wm);
//WeakMap {{id: 2} => {test_sec: 2}, {id: 1} => {test_fst: 1}}
console.log(wm.get(key1));
//{test_fst: 1}

wm.set(key3,{
    test_trd:3
});
console.log(wm);
//WeakMap {{id: 2} => {test_sec: 2}, {id: 3} => {test_trd: 3}, {…} => {test_fst: 1}}

console.log(wm.has(key3));
//true

console.log(wm.get(key3));
//{test_trd: 3}

wm.delete(key3);
console.log(wm.has(key3));
//false

console.log(wm.get(key3));
//undefined

WeakMapweak表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。进一步理解就是说,如果一个对象作为 WeakMap 的键,而且没有其他的引用指向这个对象,那么在下一次垃圾回收时,这个键值对将会被自动移除,从而释放内存。

注意:WeakMap不能遍历,方法同 getsethasdelete

综上MapWeakMap的区别:

弱映射的应用

  1. 私有变量

    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 包装起来,这样就可以把弱映射与外界完全隔离开了:

    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
    
  2. dom节点元数据

    因为 WeakMap 实例不会妨碍垃圾回收,所以非常适合保存关联元数据。来看下面这个例子,其中使用了常规的 Map

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

    假设在上面的代码执行后,页面被 JavaScript 改变了,原来的登录按钮从 DOM 树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的 DOM 节点仍然会逗留在内存中,除非明确将其从映射中

    删除或者等到映射本身被销毁。如果这里使用的是弱映射,如以下代码所示,那么当节点从 DOM 树中被删除后,垃圾回收程序就

    可以立即释放其内存(假设没有其他地方引用这个对象):

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

Set

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

const s = new Set();
let obj_zero = {
    id:0
},obj_fst = {
    id:1
};
s.add(obj_zero).add(obj_fst).add({
    id:3
});
console.log(s);
//Set(3) {size: 3, {id: 0}, {id: 1}, {id: 3}}
console.log("size:",s.size);
//size: 3
console.log("Has obj_first: ",s.has(obj_fst));
//Has obj_first:  true
s.delete({
    id:3
});
console.log("After delete: ",s);
//After delete:  Set(3) {size: 3, {id: 0}, {id: 1}, {id: 3}}

var third;
for(let i of s.keys()){
    console.log(i)
    third = i;
}
//{id: 0}
//{id: 1}
//{id: 3}

s.delete(third);
console.log("After delete: ",s);
//After delete:  Set(2) {size: 2, {id: 0}, {id: 1}}

s.clear();
console.log("After clear: ",s);
//After clear:  Set(0) {size: 0}

迭代方法

迭代器:keysvaluesentries

迭代方法:forEach

  1. forEach

    s.forEach(function(val,duplicate_val,set){
        console.log(val,duplicate_val,set);
    });
    //{id: 0} {id: 0} Set(2) {size: 2, {id: 0}, {id: 1}}
    //{id: 1} {id: 1} Set(2) {size: 2, {id: 0}, {id: 1}}
    
  2. keys

    const keys = s.keys();
    console.log("keys:",keys);//keys: SetIterator {{id: 0}, {id: 1}}
    for(let key of keys){
        console.log("key:",key);
        //key: {id: 0}
        //key: {id: 1}
    }
    console.log("keys after used: ",keys);//keys after used:  SetIterator
    
    let keys_arr = Array.from(s.keys());
    console.log("keys_arr: ",keys_arr); //keys_arr:  (2) [{id:0}, {id:1}]
    

    注意:一个迭代器不可重复使用

  3. values

    const values = s.values();
    console.log("values:",values);//values: SetIterator {{id: 0}, {id: 1}}
    
    for(let val of values){
        console.log("val:",val);
        //val: {id: 0}
        //val: {id: 1}
    }
    console.log("values after used: ",keys);//values after used:  SetIterator
    
    let values_arr = Array.from(s.values());
    console.log("values_arr: ",values_arr); // //values_arr:  (2) [{id:0}, {id:1}]
    
  4. entries

    const entries = s.entries();
    console.log("entries:",entries);//entries: SetIterator {{id: 0} => {id: 0}, {id: 1} => {id: 1}}
    
    for(let entry of entries){
        console.log("entry:",entry);
        //entry: (2) [{id:0}, {id:0}]
        //entry: (2) [{id:1}, {id:1}]
    }
    console.log("entries after used: ",entries);//entries after used:  SetIterator
    
    let entries_arr = Array.from(s.entries());
    console.log("entries_arr: ",entries_arr); //entries_arr:  (2) [[{id:0}, {id:0}], [{id:1}, {id:1}]]
    

WeakSet

WeakMapWeakSet具有以下特点:

  1. 成员都是对象(引用);
  2. 成员都是弱引用,随时可以消失,不阻止垃圾回收。可以用来保存DOM 节点,不容易造成内存泄露;
  3. 不能遍历,方法有 add、delete、has;

迭代与扩展操作

...为迭代数据类型的扩展操作符。

Li Guangqiao
Li Guangqiao

一个正在转rust的ExtJs前端工程师。迷信rust的整体发展,十分相信rust在各个领域都能发光发热,至少目前rust在很多领域上验证了其安全性、易维护性。但说实话对于我这种菜鸡也是真的难上手哈哈哈~~。 思路总结:

  • 万物诞生都会有一个需求来源,每一个改变都是为了解决某个问题,最后应该考虑如何去做
  • 学会掌握一些宏观的知识和理论:系统论、还原论
  • 工程化思想,如何描述整体,从整体架构到模块关联等 故学习东西应该像看地图一样,先看整体了解整体的结构,然后再聚焦每一个模块,对于模块的学习,思考三个问题,“是什么?”、“为什么?”、“怎么做?”;那么设计一个东西时也应该去考虑整体性和关联性。

有关于未来的发展,以下是鄙人的粗浅的观点:

  • 编程语言未来应该是每个人必备的工具
  • 未来的交互方式应该会以语言交互为主流
  • 下一个去中心化的技术方案出来之前,区块链依然是web3建立价值体系的基础技术方案,如何将现实价值和虚拟价值联通是进入数字世界的一个大难题。
  • 未来注定是AI的世界。AI的进化会伴随绝大部分人的退化,届时除了尖端人才,人们学习的重心会放在何处?