Set & Map:新生的数据集合及其弱引用衍生

2018-08-10 admin

前言

ES6新增了两种基本的原生数据集合:SetMap(加上ArrayObject现在共有四种),以及由两者衍生出的弱引用集合:WeakSetWeakMap。从某个不无狭隘的角度看(不无狭隘?到底有多狭隘多不狭隘呢?),Set更为类似Array集合的某种提升,而Map则为Object集合的增强,虽然两类在本质上就不相同。

1 Set

其本身是生成Set实例(数据集合)的构造函数,可以接受一个数组(或具有iterable接口的数据结构)作为参数用来初始化。存储的结构类似数组,不过成员的值在集合中都是唯一的,不会出现重复值。实际的存储顺序(也是遍历顺序)与插入顺序一致,行为的结果和数组相同。

其数据结构中没有键名,但为了和Map统一,也可认为键名和健值是同一值(会在遍历小节中介绍)。内部使用的相等判断规则,除了认为NaN等于NaN,与全等于一致。利用Set中没有重复值的特性,可以简单的实现数组去重。最后一点,其实例的字符串标签为[Object Set]——厉害啦,都自立了门户。

let set = new Set([1, 2]);
console.log(...set); // 1 2
console.log({}.toString.call(set)); // [Object Set]

let o = {};
[...new Set([1, 2, 2])]; // [1, 2]
[...new Set([1, NaN, NaN])]; // [1, NaN]
[...new Set([1, o, o])]; // [1, o]
[...new Set([1, {}, {}])]; // [1, {}, {}]

F(1, {}, 1); // [1, {}],可解析带遍历器的类数组对象。
function F() {
  console.log([...new Set(arguments)]);
}

let o = { length: 0 };
[...new Set(o)]; // 报错,不能自动解析不带遍历器接口的类数组对象。

// 作为简单的删除数组重复值的方法。
removeDuplicateValues([1, 2, 2, 3]); // [1, 2, 3]
function removeDuplicateValues(arr) {
  return [...new Set(arr)];
}

关于相等的小知识 两值是否相等一般指的是是否全等,里面有两个比较特殊的例子:NaN & NaN-0 & +0。在全等中,NaN不等于NaN-0等于+0都为零。但这两种认定在某些场合中不太接地气,为此ES6给出用于判断两值是否相等方法Object.is()中,认定NaN等于NaN-0不等于+0

0除以1为正零,0除以负1为负零,两者在生成上方式上看的确不应该相等。

非数NaN是一个不是数字的数字(类型依旧为数字型),没有具体的值,因此两个NaN是不相等的。不过在用NaN作为映射中的键时,它应该代指这一类型而不是具体的个体。否者我先设置代码NaN指向貂蝉,再设置NaN指代西施,晚上宽衣解带后发现仆人竟将两人同时安置在被窝之中,笑盈盈水灵灵的。这,让我如何是好!

2 Map

其本身是生成Map实例(数据集合)的构造函数,可以接受一个包含键值对的数组(或具有iterable接口的数据结构)作为参数用来初始化。简单的说,键值对是包含两元素的数组,前者为键名后者为键值。其存储的结构类似Object,不会出现重复的键名,之中使用的相等判定方法与Set一致。其实例的字符串标签为[Object Map]

其与对象主要有两点不同。一是键名,对象的键名只能是字符串或Symbol值,而Map可以是任意类型,它提供了更为完善的值对值的Hash结构。二是遍历顺序,对象的遍历顺序大致为先数值再字符串后Symbol值(会在遍历小节中介绍),而Map是简单的与存储顺序保持一致,这在实际操作中比较有用。

let map = new Map([[1, 'one'], [2, 'two']]);
console.log(...map); // [1,'one'] [2, 'two']
console.log({}.toString.call(map)); // [Object Map]

let o = {};
[...new Map([[o, 1], [o, 2]])]; // [[o, 2]]
[...new Map([[{}, 1], [{}, 2]])]; // [[{}, 2], [{}, 2]]
[...new Map([[null, 1], [undefined, 2]])]; // [[null, 1], [undefined, 2]]

let o = {
  0: [1, '1'],
  1: [2, '2'],
  length: 2,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
[...new Map(o)]; // [[1, '1'], [2, '2']]

3 实例方法

关于SetMap的完善API请点击链接查看。

这里会将SetMap放置在一起,在操作方法和遍历方法上进行异同性说明,方便区分和记忆。另外,为了在方法操作上统一SetMap,如Set小节中提及的,我们可以认为Set是键名与键值为同一值的存在(JS本身就是这样做的)。

3.1 操作方法

两者都有的操作方法 判断(has),传入键名,返回布尔值。 删除(delete),传入键名,有且成功删除为true,否则为false。 清空(clear),无需传参,没有返回值。

Set独有的操作方法 新增(add),传入值,返回实例本身。 没有相应的获取方法,因为获取需传入的值就是应所传出的值。

let set = new Set();

set.add(1).add(NaN); // set.size 为 2。
set.has(NaN); // true
set.delete(NaN); // true
set.has(NaN); // false
set.delete(2); // false
set.clear();

console.log(...set); // 它变得一无所有,只剩一具空壳。

Map独有的操作方法 新增(set),传入键名和键值,有则更新没则新增,返回实例本身。 获取(get),传入键名,返回相应值没有则为undefined

let o = [1, 2, 3];
let map = new Map();

map.set(o, 1).set(o, 2); // map.size 为 1
map.has(o); // true
map.get(o); // 2
map.get([1, 2, 3]); // undefined
map.delete(o); // true
map.clear();

console.log(...map); // 它再次一无所有,只剩愈发饥渴的兽心。

3.2 遍历方法

返回键名的遍历器对象(keys)。 返回键值的遍历器对象(values)。 返回键值对的遍历器对象(entries),键值对为[键名, 键值]。 遍历每个成员(forEach),使用方式与Array的方法相同。

因为Set的键名和键值相同,所以一般只使用values方法获取全部值。而Map则根据相应需求获取即可。

let set = new Set([1, 2, 3]);
let map = new Map([[1, 'one'], [null, NaN]]);

[...set.values()]; // [1, 2, 3]
[...set.keys()]; // [1, 2, 3]
[...set.entries()]; // [[1, 1], [2, 2], [3, 3]]

[...map.values()]; // ['one', NaN]
[...map.keys()]; // [1, null]
[...map.entries()]; // [[1, 'one'], [null, NaN]]

两者的forEach方法与数组的不同点在于回调函数的第二个参数,前者为该项的键名后者为该项的序号。

let set = new Set([1, 2, 3]);
let map = new Map([[1, 'one'], [null, NaN]]);

set.forEach((v, i) => console.log(i)); // 1, 2, 3
map.forEach((v, i) => console.log(i)); // 1, null
[1, 2, 3].forEach((v, i) => console.log(i)); // 0, 1, 2

对象属性的遍历顺序 不同遍历对象的方法面向的数据种类不同,但总的说其遍历顺序是这样的:先找到其中可转化成数值的属性并按升序遍历,再遍历字符串属性按加入时间的前后,最后遍历Symbol值按加入时间的前后。Map的遍历顺序即其被插入时的顺序,嗯,总有些色咪咪的味道。

let obj = {
  '0': 0,
  1: 1,
  'b': 2,
  'a': 3,
  [Symbol(2)]: 4,
  [Symbol(1)]: 5
};

Reflect.ownKeys(obj); // ["0", "1", "b", "a", Symbol(2), Symbol(1)]

4 弱引用集合

弱引用集合WeakSetWeakMap是由SetMap分别衍生出的,其与本体的异同点一致,因此只对WeakMap进行说明。

在JS的垃圾回收机制中,对象会在没有引用时(可意为使用)被回收。这说明着,如果有个数据集合(数组、对象、SetMap)中包含了某对象(在使用它),那么在此数据集合被回收之前该对象都不能被回收。这很容易导致内存泄漏(专有名词,可简单理解为内存被没用的数据占据)。

弱引用集合的设计目的就是为了解决这个问题。弱引用顾名思义是指虽然某对象被此集合引用了,但该引用不被引擎保护,不被垃圾回收装置考虑在内,该回收时就得乖乖的被回收。那些被惯成畸形的家伙们,要知道,妈妈的怀抱可不是个万全的地方哦,唯有死神的才是。

WeakMap的行为与Map除了以下几点不同外,可以认为是一致的。 WeakMap的键名只能是对象(不包括null),否者报错。如果能放入普通类型,那有什么意义呢? WeakMap的键名是动态不定的,不知道什么时候会被回收。键名指代的对象被回收后,该项会被自动消除。 因为项数的动态性,所以不能被遍历(没有遍历方法),没有size属性,没有cealr方法。

let o = {};
let wm = new WeakMap();

wm.set(1, 1); // 报错,1 不是对象。
wm.set(o, 1);
wm.has(o); // true
wm.get(o); // 1
wm.delete(o); // true

'size' in wm; // false
'clear' in wm; // false
'values' in wm; // false

弱引用集合的优点在于,我们可以任意为其注册对象,而不用担心内存泄漏。典型的应用场景是将DOM与数据进行绑定。一些要在DOM中绑定数据的库中,比如d3,会直接在DOM对象上设置属性进行保存。但在日常组建单页面程序中的某个阶段,想将DOM与数据联系在一起时,我们显然会优先选用数据映射的方式。而弱引用集合的出现,更加优化了这种方式。

在下面的示例中,每次点击请求数据后都会生成帮了相应数据项的li标签,并将该标签与相应的数据进行绑定。在这一系列轮回存储绑定中,因为WeakMap的弱引用特性,我们不需要关心已经被删除的DOM元素。每次只需进行相同的操作,方便安心,省时省力。

<button onclick="requestData()">Request Data</button>

<ul id="container"></ul>

<script>
  "use strict";

  let wmap = new WeakMap();
  let container = document.querySelector('#container');

  function requestData() {
    container.innerHTML = '';

    [0, 0, 0].map(() => Math.random()).forEach(d => {
      let li = document.createElement('li');
      li.innerHTML = `
        <button onclick="showMes(this)">Show Message</button>
        <button onclick="deleteItem(this)">Delete Item</button>
      `;
      wmap.set(li, d);
      container.appendChild(li);
    });
  }

  function showMes(that) {
    let li = that.parentNode;
    li.innerHTML = wmap.get(li);
  }
  function deleteItem(that) {
    let li = that.parentNode;
    container.removeChild(li);
  }
</script>

延伸阅读

ES6精华:Symbol ES6精华:解构赋值 ES6精华:函数扩展 ES6精华:Proxy & Reflect Iterator:访问数据集合的统一接口

原文链接:https://segmentfault.com/a/1190000015953853

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-37673.html

文章标题:Set & Map:新生的数据集合及其弱引用衍生

相关文章
12个你未必知道的CSS小知识
虽然CSS并不是一种很复杂的技术,但就算你是一个使用CSS多年的高手,仍然会有很多CSS用法/属性/属性值你从来没使用过,甚至从来没听说过。 1.CSS的color属性并非只能用于文本显示 对于CSS的color属性,相信所有Web开发人员...
2015-11-12
ajax为什么令人惊异?ajax的优缺点
使用Ajax的最大优点,就是能在不更新整个页面的前提下维护数据。这使得Web应用程序更为迅捷地回应用户动作,并避免了在网络上发送那些没有改变的信息。 Ajax不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。就像DHT...
2015-11-12
HTML5的5个不错的开发工具推荐
HTML5规范终于在今年正式定稿,对于从事多年HTML5开发的人员来说绝对是一个重大新闻。数字天堂董事长,DCloud CEO王安也发表了文章,从开发者和用户两个角度分析了HTML对两个人群的优势。其实,关于HTML5的开发工具,我们以往的...
2015-11-12
JavaScript教程:JS中的原型
Keith Peters 几年前发表的一篇博文,关于学习没有“new”的世界,其中解释了使用原型继承代替构造函数。两者都是纯粹的原型编码。 标准方法(The Standard Way) 一直以来,我们学习的在 JavaScript 里创建对...
2015-11-12
AJAX的浏览器支持
AJAX 的要点是 XMLHttpRequest 对象。 不同的浏览器创建 XMLHttpRequest 对象的方法是有差异的。 IE 浏览器使用 ActiveXObject,而其他的浏览器使用名为 XMLHttpRequest 的 Jav...
2015-11-12
Riot.js:不足1KB的MVP客户端框架
Riot.js是一款MVP(模型-视图-呈现)开源客户端框架,其最大的特点就是体积非常小,不足1KB,虽然体积小,但它可以帮助用户构建大规模的Web应用程序。 Riot.js是由Moot公司开发,目前最新版本为v0.9.2,遵循MIT开源许...
2016-03-11
JavaScript的组成
一个完整的JavaScript由3个部分组成:核心(ECMAScript) 文档对象模型(DOM) 浏览器对象模型(BOM) ECMAScript 描述了该语言的语法和基本对象 ; DOM 描述了处理网页内容的方法和接口 ; BOM 描...
2015-11-12
typeof、instanceof和contructor的区别
typeof:以字符串的形式返回变量的原始类型,typeof在两种情况下会返回&quot;undefined&quot;:一个变量没有被声明的时候,和一个变量的值是undefined的时候,注意,typeof null也会返回object,...
2015-11-12
必须记住的 30 类 CSS 选择器
开篇 有 30 个 CSS 选择器你必须烂熟于心,它们适应于当今各大主流浏览器。 1.* * { margin: 0; padding: 0; } *选择器选择的是每一个单一元素。很多程序员用上面的 CSS 将所有元素的 ma...
2015-11-16
JavaScript变量的声明
声明变量 变量在脚本中的首次亮相是在其声明中。 在变量首次出现时将会在内存中设置它,因此您稍后可在脚本中引用它。 应在使用变量之前先声明变量。 可以使用 var 关键字实现此目的。 &lt;span id=“mt9” class=“sent...
2015-11-12
回到顶部