【重走JavaScript之高级程序设计】集合和映射

1.Map 映射

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值)都可以作为键或值。

1.1 创建Map

// 使用New关键字和Map构造函数初始化映射
const map = new Map();
// 使用嵌套数组初始化映射
const map = new Map([
  ["key1", "value1"],
  ["key2", "value2"],
  ["key3", "value3"]
]);
// 使用自定义迭代器初始化映射
const map = new Map({
  [Symbol.iterator]: function* () {
    yield ["key1", "value1"];
    yield ["key2", "value2"];
    yield ["key3", "value3"];
  }
});

1.2 基本API

  • set(key,value) 添加键值对

  • get(key) 获取键对应的值

  • has(key) 判断是否存在键值对

  • delete(key) 删除键值对

  • size 获取Map的键/值对数量(长度)

  • clear() 清空Map所有键值对

const map = new Map();
// 1. 添加元素,set可以链式调用
map.set("firstName", "paul");
   .set("lastName", "oldVal");
   .set("lastName", "walker"); // set添加重复键的时候变为更新,理解为修改已有键的关联值

// 2. 获取元素
map.get("firstName"); // "paul"
map.get("lastName"); //	"walker"

// 3. 删除元素
map.delete("firstName"); // true
map.delete("abc"); // false

// 4. 判断是否存在
map.has("firstName"); // false
map.has("lastName"); // true

// 5. 获取Map的长度
map.size; // 1

// 6. 清空Map
map.clear(); // Map(0) {size: 0}

1.3 键匹配方式

Map 内部使用SameValueZero(ECMA内部规范) 比较操作,几乎可以理解为严格相等来检查键的匹配性。

// 由于使用严格相等,在映射中用作键盘/值的对象及其他“集合”类型,在自己的内容或属性被修改时仍然保持不变。

const map = new Map();
const objKey = {},
  objVal = {},
  arrKey = [],
  arrVal = [];

map.set(objKey, objVal);
map.set(arrKey, arrVal);

objKey.foo = "foo";
objVal.bar = "bar";
arrKey.push("foo");
arrVal.push("bar");

map.get(objKey); // {"bar": "bar}
map.get(arrKey); // ['bar']

1.4 顺序与迭代

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

  1. 映射实例可以提供一个迭代器(Iterator),能以插入顺序生成[key,value]形式的数组。通过 entries()方法(或者Symbol.iterator属性,它也是引用entries)取得这个迭代器。
const map = new Map(["key1", "val1"], ["key2", "val2"], ["key3", "val3"]);

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

// 使用entres取得迭代器
for (let pair of map.entries()) {
  console.log(pair);
}
// ["key1", "val1"]
// ["key2", "val2"]
// ["key3", "val3"]

// 使用Symbol.iterator取得迭代器
for (let pair of map[Symbol.iterator]){
	console.log(pair);
}
// ["key1", "val1"]
// ["key2", "val2"]
// ["key3", "val3"]

  1. 使用扩展运算符对映射使用扩展操作,把映射转换为数组。和使用Map构造函数将数组转换为映射互为逆操作
const m = [...map];
console.log(m); // ["key1", "val1", "key2", "val2", "key3", "val3"]
  1. 使用数组方法迭代
// 使用forEach方法
const map = new Map(["key1", "val1"], ["key2", "val2"], ["key3", "val3"]);
map.forEach((val, key) => console.log(`${val} => ${key}`));
// val1 => key1
// val2 => key2
// val3 => key3

// 使用keys方法
for (let key of map.keys()) {
  console.log(key);
}
// key1
// key2
// key3

// 使用values方法
for (let val of map.values()) {
  console.log(val);
}
// val1
// val2
// val3
  1. 键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法西拐。当然,这并不妨碍修改作为键或值的对象内部的属性,因为这样并不影响它们在映射实例中的身份。
// 作为键的字符串原始值是不能修改的
const map1 = new Map(["key1", "val1"]);
for (let key of map.keys()) {
  key = "newKey";
  console.log(key); // newKey
  console.log(map.get("key1")); // val1
}

// 修改了作为键的对象的属性,但对象在映射内部仍然引用相同的值
const keyObj = { id: 1 };
const map2 = new Map([keyObj, "val1"]);
for (let key of map2.keys()) {
  key.id = "newKey";
  console.log(key); // {id : "newKey"}
  console.log(map2.get(keyObj)); // val1
}
console.log(keyObj); // {id: "newKey"}

1.5 Map 与 Object 的区别(选择Object还是Map)

ES6之前中键/值对使用Object实现

  1. Object只能使用数值、字符串或符号作为键不同,Map可以使用任何JavaScript 数据类型作为键。两者映射的值都是没有限制的。

  2. 键值对的顺序。Object的键值对是无序的,Map的键值对是按照传入顺序维护的的。

  3. 迭代方法。在遍历 Map 时,我们需要使用 map.entries() 方法,然后使用 for...of 循环或 Array.from() 方法遍历。对于 Object,我们可以直接使用 for...in 循环或 Object.keys()、Object.values()、Object.entries() 方法。

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

  5. 插入速度。如果涉及大量插入操作,Map性能更佳。

  6. 查找速度。少量键值对Object速度更快,浏览器对连续整数作为键的Object有优化,速度更快。

  7. 删除性能。Object使用delete性能堪忧,所以出现伪删除。Map的delete方法性能更好。

2.WeakMap 弱映射

WeakMap是一种新的集合类型。WeakMap是Map的兄弟类型,其API是Map的子集。weak弱描述的是JavaScript的垃圾回收机制对弱映射中键的方式。

它与Map的区别在于,WeakMap的键只能是Object或继承自Object的类型,而不能是其他类型。值的类型没有限制。

2.1 基本API

  • set(key,value) 添加键值对

  • get(key) 获取键对应的值

  • has(key) 判断是否存在键值对

const key1 = { id: 1 },
  key2 = { id: 2 },
  key3 = { id: 3 };

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

wm1.get(key1); // "val1"
wm1.has(key1); // true

// set可以链式调用
const key4 = { id: 4 };
const key5 = { id: 5 };
wm1.set(key4, "val4").set(key5, "val5");

wm1.delete(key1); // true

// 初始化是全有或全无的操作,只要一个键无效就会抛出错误
const wm2 = new WeakMap([
  [key1, "val1"],
  ["error", "val2"],
  [key3, "val3"]
]);

2.2 理解弱键

WeakMap中weak 表示弱映射的键是“弱弱的拿着”。意思就是这些键不属于正式的引用,不会阻止垃圾回收。

但是弱映射中值可不是“弱弱的拿着”。只要键存在,键/值对就会存在于映射中,并被当作对值对引用,因此不会被当作垃圾回收。

// 空对象作为键,没有指向这个键的其他引用,会被垃圾回收,键/值对从弱映射中消失,成为一个空映射。
const wm1 = new WeakMap();
wm1.set({}, "val");
// WeakMap在内部使用了一个特殊的算法来处理垃圾回收。
// 控制台中看到的输出结果可能仍然显示WeakMap包含被垃圾回收的键值对。但是实际上,这个键值对已经不存在于WeakMap中了。
wm1.get({}); // undefined

// 此时container对象维护着一个对弱映射 键 对引用,因此不会被垃圾回收。
const wm2 = new WeakMap();
const container = { key: {} };
wm2.set(container.key, "val");
wm2.get(container.key); // val
// 但是如果摧毁键对象的引用,垃圾回收程序将会清理这个键/值对。
function removeReference() {
  container.key = null;
}
// 调用函数清除引用
removeReference();
// 清除完引用,键/值对消失
wm2.get(container.key); // undefined

2.3 不可迭代键

WeakMap中 键/值对任何时候都可能销毁,所以没必要提供其键/值对的能力。也不用clear方法一次性销毁所有键/值对。

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

2.4 使用弱映射

  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.property);
  }
}

const user = new User(123);
console.log(user.getId()); // 123
user.setId(456);
console.log(user.getId()); // 456
  1. DOM 节点元数据
const m = new Map();
// 就算按钮在后续执行过程中从DOM树移除了,由于映射还保存着按钮的引用,所以对应的DOM节点仍然在内存中。
// 除非手动将其从映射中删除或映射本身被销毁。
const btn = document.querySelector("#login");
// 给这个节点关联一些元数据
m.set(btn, { disabled: true });
const wm = new WeakMap();
// 由于使用的是弱映射,当节点从DOM树移除后,垃圾回收程序会立即释放其内存。
const btn = document.querySelector("#login");
// 给这个节点关联一些元数据
m.set(btn, { disabled: true });

3.Set 集合

Set 是一种新的集合类型,为JavaScript带来集合数据结构。Set可以包含任何JavaScript数据类型.Set在很多方面都像是加强的Map,很多API共用。

3.1 创建Set

// 使用New关键字和Set构造函数初始化映射
const map = new Set();
// 使用嵌套数组初始化集合
const map = new Map(["val1", "val2", "val3"]);
// 使用自定义迭代器初始化映射
const map = new Map({
  [Symbol.iterator]: function* () {
    yield "val1";
    yield "val2";
    yield "val3";
  }
});

3.2 基本API

  • add(value) 添加值

  • has(value) 判断是否存在值,返回布尔值

  • delete(value) 删除值

  • size 获取Set集合的数量(长度)

  • clear() 清空Set实例所有值

const s = new Set();
// 1. 添加元素,set可以链式调用
s.add("paul").add("walker");

// 2. 查询元素是否存在
s.has("paul"); // true
s.has("walker"); //	true

// 3. 删除元素
s.delete("paul"); // true
s.delete("abc"); // false

// 4. 判断是否存在
s.has("paul"); // false
s.has("walker"); // true

// 5. 获取Set的长度
s.size; // 1

// 6. 清空Set
s.clear(); // Set(0) {size: 0}

3.3 键匹配方式

Set 内部使用SameValueZero(ECMA内部规范) 比较操作,几乎可以理解为严格对象相等的标准来检查值的匹配性。

// 由于使用严格相等,用作值的对象和其他“集合”类型,在自己的内容或属性被修改时仍然保持不变。

const s = new Set();
const objVal = {},
  arrVal = [];

s.add(objVal);
s.add(arrVal);

objVal.bar = "bar";
arrVal.push("bar");

s.has(objVal); // true
s.has(arrVal); // true

3.4 顺序迭代

Set 会维护值插入时的顺序,因此支持按顺序迭代。(Map和Set都有顺序)

  1. 集合实例可以提供一个迭代器(Iterator),能以插入顺序生成集合内容。可以通过values()方法及其别名方法keys()(或者Symbol.iterator属性,它引用values())取得着个迭代器
const s = new Set(["val1", "val2", "val3"]);

s.values === s[Symbol.iterator]; // true
s.keys === s[Symbol.iterator]; // true

// 使用values取得迭代器
for (let value of s.values()) {
  console.log(value);
}
// value1
// value2
// value3

// 使用Symbol.iterator取得迭代器
for (let value of map[Symbol.iterator]()) {
  console.log(value);
}
// value1
// value2
// value3

// 使用entries取得迭代器,两个元素重复
for (let pair of s.entries()) {
  console.log(pair);
}

// ["val1", "val1"]
// ["val2", "val2"]
// ["val3", "val3"]
  1. 使用扩展运算符对映射使用扩展操作,把映射转换为数组。和使用Map构造函数将数组转换为映射互为逆操作
const s = new Set(["val1", "val2", "val3"]);
const arr = [...s];
console.log(arr); // ["val1", "val2", "val3"]
  1. 使用数组方法迭代
// 使用forEach方法
const s = new Set(["val1", "val2", "val3"]);
s.forEach((val, dupVal) => console.log(`${val} => ${dupVal}`));
// val1 => val1
// val2 => val1
// val3 => val1

// 使用values方法
// 修改集合中值的属性不会影响作为集合值的身份
// 字符串原始值不会被修改
for (let value of s.values()) {
  value = "newValue";
  console.log(value); // newValue
  console.log(s.has("val1")); // true
}
// key1
// key2
// key3

const valObj = { id: 1 };

const s2 = new Set([valObj]);

// 修改值对象的属性,但对象仍存在于集合中
for (let value of s2.values()) {
  value.id = "newVal";
  console.log(value); // {id:newVal}
  console.log(s2.has(valObj)); // true
}

console.log(valObj); // {id:newVal}

3.5 集合的操作

Set和Map很相似,只是API稍有调整

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(n) {
    return xSet.powerSet(this);
  }

  // 返回两个或更多集合的并集
  static union(a, ...bSets) {
    // 初始化集合a
    const unionSet = new XSet(a);
    // 从b中迭代插入a,自动去重,取得并集
    for (const b of bSets) {
      for (const bValue of b) {
        unionSet.add(bValue);
      }
    }
    return unionSet;
  }

  // 返回两个或更多集合的交集
  static intersection(a, ...bSets) {
    // 初始化集合a
    const intersectionSet = new XSet(a);
    // 迭代a中每一项,判断b中是否存在该项,若没有删除a中该项取得交集
    for (const aValue of intersectionSet) {
      for (const b of bSets) {
        if (!b.has(aValue)) {
          intersectionSet.delete(aValue);
        }
      }
    }
    return intersectionSet;
  }

  // 返回两个集合的交集
  static difference(a, b) {
    // 初始化集合a
    const differenceSet = new XSet(a);
    // 迭代b中每一项,判断a中是否存在该项,若存在删除a中该项取得差集
    for (const bValue of b) {
      if (a.has(bValue)) {
        differenceSet.delete(bValue);
      }
    }
    return differenceSet;
  }

  // 返回两个集合的对称差集
  static symmetricDifference(a, b) {
    // 按照定义,对称差集可以表达为
    return s.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)) {
        powerSetSet.add(new XSet(set).add(aValue));
      }
    }
    return powerSet;
  }
}

4.WeakSet 弱集合

WeakSet是一种新的集合类型。WeakSet是Set的兄弟类型,其API是Set的子集。weak弱描述的是JavaScript的垃圾回收机制对弱集合中键的方式。

它与Map的区别在于,WeakSet的键只能是Object或继承自Object的类型,而不能是其他类型。值的类型没有限制。

2.1 基本API

  • add(value) 添加值

  • has(value) 判断是否存在值

  • delete(key) 获取键对应的值

const val1 = { id: 1 },
  val2 = { id: 2 },
  val3 = { id: 3 };

const ws1 = new WeakSet(["val1", "val2", "val3"]);

ws1.has(val1); // true
ws1.has(val2); // true

// add可以链式调用
const val4 = { id: 4 };
const val5 = { id: 5 };
ws1.add(val4).set(val5);

ws1.delete(key1); // true

// 初始化是全有或全无的操作,只要一个键无效就会抛出错误
const wm2 = new WeakSet([val1, "badVal", val3]);

2.2 理解弱值

WeakSet中weak 表示弱集合的键是“弱弱的拿着”。意思就是这些键不属于正式的引用,不会阻止垃圾回收。

// 空对象作为值,没有指向这个对象的其他引用,会被垃圾回收,值对从弱映射中消失,成为一个空集合。
const ws1 = new WeakSet();
ws1.add({});
ws1.has({}); // undefined

// container对象维护一个对弱集合值的引用,因此着个对象不会成为垃圾回收的目标。
const ws2 = new WeakSet();
const container = {
  val: {}
};
ws2.add(container.val);
// 但是如果摧毁值对象的引用,垃圾回收程序将会清理这个值对。
function removeReference() {
  container.val = null;
}
removeReference();

// 清除完引用,键/值对消失
ws2.has(container.val); //undefined

2.3 不可迭代键

WeakSet中 值对任何时候都可能销毁,所以没必要提供其值的能力。也不用clear方法一次性销毁所有键/值对。

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

2.4 使用弱映射

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

// 查询元素是否在禁用列表中,假如元素从DOM树删除了,引用仍存在,垃圾回收程序不能回收它。
const disabledElements = new Set();
const btn = document.querySelector("#login");
// 通过加入对应集合,给这个节点打上禁用标签.
disabledElements.add(btn);

// 使用弱映射,垃圾回收程序就会回收元素的内存。
const disabledElements = new WeakSet();
const btn = document.querySelector("#login");
// 通过加入对应集合,给这个节点打上禁用标签.
disabledElements.add * btn;
posted @   wanglei1900  阅读(33)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示