初探 ES6 Sets - Barbarian Meets Coding

97 阅读7分钟

译者:huangxiaolu

原文链接

Mastering the Arcane Art of JavaScript-mancy series 系列里,我尝试将我对 Javascript 的爱带给其他 C# 开发者,他们可能还不知道这门语言和它的整个生态系统有多棒。这一系列文章摘自我的大作 JavaScript-Mancy book ,这本书涵盖了 C# 开发者需要了解的关于 javascript 的所有知识。

Set 是一个数据结构,用来表示一个特定集合,该集合里的元素互不相等。如果你曾经用过 Javascript,在过去你需要自己实现一个 Set。现在好了,ES6 已经实现了原生的 Set

使用 Set

你可以在这个 jsBin 里亲自试验本文里的所有例子

可以用 Set 构造函数创建一个新的 set:

let set = new Set();

或者用一个可迭代的集合,比如数组来创建一个 set:

let elementsOfMagic = new Set(['earth', 'fire', 'air', 'earth', 'fire', 'water']);

console.log(`These are the elements of magic: ${[...elementsOfMagic]}`);
// => These are the elements of magic: earth, fire, air, water

从上面的例子就能看到,一个 Set 会自动去掉重复元素,一个元素只存一次。使用 add 方法能够轻松地添加更多的元素到 Set

elementsOfMagic.add('aether');

console.log(`More magic!: ${[...elementsOfMagic]}`);
// => More magic!: earth, fire, air, water, aether

add方法可以链式调用,添加多个新元素很方便:

elementsOfMagic.add('earth').add('air').add('water');

has 方法来检查元素是否存在于一个 Set 中:

console.log(`Is water one of the sacred elements of magic? ${elementsOfMagic.has('water')}`)
// => Is water one of the sacred elements of magic? true

delete 方法来移除元素:

elementsOfMagic.delete('aether');

console.log(`The aether element flows like the tides and
like the tides sometimes disappears:
${[...elementsOfMagic]}`);

// => The aether element flows 
//    like the tides and sometimes disappears: 
//    earth,fire,air,water

另外,用 size 方法能得到 set 里的元素个数:

console.log(${elementsOfMagic.size} are the elements of magic);

clear 方法能移除 set 里的所有元素:

const castMagicShield = () => elementsOfMagic.clear();
castMagicShield();

console.log(`ups! I can't feel the elements: ${elementsOfMagic.size}`);
// => ups! I can't feel the elements: 0

花一分钟回想一下刚才所讲的 Set API,再回忆前一篇文章里介绍的 Map,你会发现两者非常相似。相似的好处是能快速学习这些 API,而且还不容易写错代码。接下来看看如何迭代一个 Set 里的元素。

迭代 Set

Map 一样,用 for/of 循环来迭代一个 Set 的元素:

elementsOfMagic.add('fire').add('water').add('air').add('earth');
for(let element of elementsOfMagic){
  console.log(`element: ${element}`);
}
// => element: fire
//    element: water
//    element: air
//    element: earth

在这个例子里,迭代的是 Set 的每个元素,而不是键/值对。我们注意到元素迭代的顺序与插入 set 的顺序保持一致。 Set 默认使用的是 values 迭代器。下面这段代码等价于上面的代码:

for(let element of elementsOfMagic.values()){
  console.log(`element: ${element}`);
}

Map 一样, Set 也有对 keysentries 的迭代器,但也许你用不到。 keys 迭代器也是对 Set 的元素集合进行迭代(等价于 values )。 entries 迭代器将每个元素转换成键/值对,键和值都等于元素值。所以使用 entries 迭代器相当于迭代 [value, value] 对。

除了这些迭代器,你还可以借助 Set.prototype.forEach 方法遍历 Set 的元素。

elementsOfMagic.forEach((value, alsoValue, set) => {
  console.log(`element: ${value}`);
})
// => element: fire
//    element: water
//    element: air
//    element: earth

Set 的 Array 方法

SetArray 之间的相互转换很简单,因此,我们可以轻松地使用 Array.prototype 对象里的所有方法:

function filterSet(set, predicate){
  var filteredItems = [...set].filter(predicate);
  return new Set(filteredItems);
}

var aElements = filterSet(elementsOfMagic, e => e.startsWith('a'));
console.log(`Elements of Magic starting with a: ${[...aElements]}`);
// => Elements of Magic starting with a: air

在关于_Array的文章_里提到了很多这些方法,你还可以在 关于 JavaScript 和 LINQ 的另一篇文章里 找到更多方法。

Set 如何判断相同元素?

现在你已经知道在添加元素到 Set 时,它会去掉重复元素。但是它怎么判断两个元素是否想等呢?

好吧… 它用了严格相等比较(也就是大家熟知的 === 或者 !==)来进行判断。理解这点很重要,因为在实际应用中,这种比较方式会造成很大的限制。因为尽管严格相等比较在判断数和字符串时没问题,但比较对象的时候,是对引用进行比较,也就是说,两个对象只有是同一个对象的时候才会相等。

下面用一个例子来说明这个特殊场景。假设有一个关于人的 Set,当然每个人都是独一无二的个体(我们每个人都像珍贵的宝石一样,是造物主创造的美丽的奇迹):

let persons = new Set();

我们创造了一个人的对象 randalf,然后尝试将其两次添加到 Set 中:

let randalf = {id: 1, name: 'randalf'};

persons
	.add(randalf)
	.add(randalf);

console.log(`I have ${persons.size} person`)
// => I have 1 person 

console.log([...persons]);
// => [[object Object] {
//  id: 1,
//  name: ""randalf""
//}]

Set 很好地支持了我们的需求,只添加了一次。因为是同样的对象,在这种场景下使用严格相等比较很合适。但是,在特殊场景下,我们想添加一个我们认为相等的对象,会发生什么呢?假设,我们认为只要两个人有一样的属性,特别是有一样的 id,那么两个人就是相等的(因为我相信周围有很多 randalfs ,尽管我从来没见过他们):

persons.add({id: 1, name: 'randalf'});
console.log(`I have ${person.size} persons?!?`)

// => I have 2 persons?!?
console.log([...persons]);
/*
*= [[object Object] {
  id: 1,
  name: ""randalf""
}, [object Object] {
  id: 1,
  name: ""randalf""
}]
*/

在这种情况下,对象会被添加到 Set 里,从各方面来看,相同的人都会出现两次。不幸的是,现在还没有办法在 Set 里指定元素相等的标准,我们只能期待将来 JavaScript 里能加入这个功能。

但是我们可以想象一下这个功能长什么样,比如,下面这种方式应该会很棒:

let personsSet = new Set([], p => p.id); `

目前,假如你要使用 Set 来实现存储对象的功能,你最好使用字典索引对象,用一个 key 来代表对象的唯一性。

var fakeSetThisIsAHack = new Map();
fakeSetThisIsAHack
  .set(randalf.id, randalf)
  .set(1, {id: 1, name: 'randalf'});
console.log(`fake set has ${fakeSetThisIsAHack.size} item`);
// => fake set has 1 item

总结

Set 是 ES6 里一种新的数据结构,能帮你轻松地移除集合里的重复元素。它提供了很简单的 API,并且跟 Map 的 API 保持了一致。它能提供很强大的帮助,省得你在程序里再实现自己的 Set。不幸的是,现在它的局限性在于只支持用严格相等比较的方式来判断两个元素是否相等。希望在将来我们能够自己定义相等的方式,到那时候, Set 就能发挥真正的潜能了。在那之前,只对数和字符串用 Set,处理对象的时候,就用 Map吧。

快来买我的书!

你是想学 JavaScript 的 C# 开发者么? 那就快来看看这本书:JavaScript-mancy book,一个写给 C# 开发者的全面的 JavaScript 手册。

a JavaScriptmancy sample cover

祝你周末愉快!! :)

该系列的其它文章