基本介绍

ES6 提供了新的数据结构 Set。

它类似于数组,但是成员的值都是唯一的,没有重复的值。

初始化

Set 本身是一个构造函数,用来生成 Set 数据结构。

  1. let set = new Set();

Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

  1. let set = new Set([1, 2, 3, 4, 4]);
  2. console.log(set); // Set(4) {1, 2, 3, 4}
  3.  
  4. set = new Set(document.querySelectorAll('div'));
  5. console.log(set.size); // 66
  6.  
  7. set = new Set(new Set([1, 2, 3, 4]));
  8. console.log(set.size); // 4

属性和方法

操作方法有:

  • add(value):添加某个值,返回 Set 结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为 Set 的成员。
  • clear():清除所有成员,无返回值。
    举个例子:
  1. let set = new Set();
  2. console.log(set.add(1).add(2)); // Set [ 1, 2 ]
  3.  
  4. console.log(set.delete(2)); // true
  5. console.log(set.has(2)); // false
  6.  
  7. console.log(set.clear()); // undefined
  8. console.log(set.has(1)); // false

之所以每个操作都 console 一下,就是为了让大家注意每个操作的返回值。

遍历方法有:

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员,无返回值
    注意 keys()、values()、entries() 返回的是遍历器
  1. let set = new Set(['a', 'b', 'c']);
  2. console.log(set.keys()); // SetIterator {"a", "b", "c"}
  3. console.log([...set.keys()]); // ["a", "b", "c"]
  1. let set = new Set(['a', 'b', 'c']);
  2. console.log(set.values()); // SetIterator {"a", "b", "c"}
  3. console.log([...set.values()]); // ["a", "b", "c"]
  1. let set = new Set(['a', 'b', 'c']);
  2. console.log(set.entries()); // SetIterator {"a", "b", "c"}
  3. console.log([...set.entries()]); // [["a", "a"], ["b", "b"], ["c", "c"]]
  1. let set = new Set([1, 2, 3]);
  2. set.forEach((value, key) => console.log(key + ': ' + value));
  3. // 1: 1
  4. // 2: 2
  5. // 3: 3

属性:

  • Set.prototype.constructor:构造函数,默认就是 Set 函数。
  • Set.prototype.size:返回 Set 实例的成员总数。

模拟实现第一版

如果要模拟实现一个简单的 Set 数据结构,实现 add、delete、has、clear、forEach 方法,还是很容易写出来的,这里直接给出代码:

  1. /**
  2. * 模拟实现第一版
  3. */
  4. (function(global) {
  5.  
  6. function Set(data) {
  7. this._values = [];
  8. this.size = 0;
  9.  
  10. data && data.forEach(function(item) {
  11. this.add(item);
  12. }, this);
  13. }
  14.  
  15. Set.prototype['add'] = function(value) {
  16. if (this._values.indexOf(value) == -1) {
  17. this._values.push(value);
  18. ++this.size;
  19. }
  20. return this;
  21. }
  22.  
  23. Set.prototype['has'] = function(value) {
  24. return (this._values.indexOf(value) !== -1);
  25. }
  26.  
  27. Set.prototype['delete'] = function(value) {
  28. var idx = this._values.indexOf(value);
  29. if (idx == -1) return false;
  30. this._values.splice(idx, 1);
  31. --this.size;
  32. return true;
  33. }
  34.  
  35. Set.prototype['clear'] = function(value) {
  36. this._values = [];
  37. this.size = 0;
  38. }
  39.  
  40. Set.prototype['forEach'] = function(callbackFn, thisArg) {
  41. thisArg = thisArg || global;
  42. for (var i = 0; i < this._values.length; i++) {
  43. callbackFn.call(thisArg, this._values[i], this._values[i], this);
  44. }
  45. }
  46.  
  47. Set.length = 0;
  48.  
  49. global.Set = Set;
  50.  
  51. })(this)

我们可以写段测试代码:

  1. let set = new Set([1, 2, 3, 4, 4]);
  2. console.log(set.size); // 4
  3.  
  4. set.delete(1);
  5. console.log(set.has(1)); // false
  6.  
  7. set.clear();
  8. console.log(set.size); // 0
  9.  
  10. set = new Set([1, 2, 3, 4, 4]);
  11. set.forEach((value, key, set) => {
  12. console.log(value, key, set.size)
  13. });
  14. // 1 1 4
  15. // 2 2 4
  16. // 3 3 4
  17. // 4 4 4

模拟实现第二版

在第一版中,我们使用 indexOf 来判断添加的元素是否重复,本质上,还是使用 === 来进行比较,对于 NaN 而言,因为:

  1. console.log([NaN].indexOf(NaN)); // -1

模拟实现的 Set 其实可以添加多个 NaN 而不会去重,然而对于真正的 Set 数据结构:

  1. let set = new Set();
  2. set.add(NaN);
  3. set.add(NaN);
  4. console.log(set.size); // 1

所以我们需要对 NaN 这个值进行单独的处理。

处理的方式是当判断添加的值是 NaN 时,将其替换为一个独一无二的值,比如说一个很难重复的字符串类似于 @@NaNValue,当然了,说到独一无二的值,我们也可以直接使用 Symbol,代码如下:

  1. /**
  2. * 模拟实现第二版
  3. */
  4. (function(global) {
  5.  
  6. var NaNSymbol = Symbol('NaN');
  7.  
  8. var encodeVal = function(value) {
  9. return value !== value ? NaNSymbol : value;
  10. }
  11.  
  12. var decodeVal = function(value) {
  13. return (value === NaNSymbol) ? NaN : value;
  14. }
  15.  
  16. function Set(data) {
  17. this._values = [];
  18. this.size = 0;
  19.  
  20. data && data.forEach(function(item) {
  21. this.add(item);
  22. }, this);
  23.  
  24. }
  25.  
  26. Set.prototype['add'] = function(value) {
  27. value = encodeVal(value);
  28. if (this._values.indexOf(value) == -1) {
  29. this._values.push(value);
  30. ++this.size;
  31. }
  32. return this;
  33. }
  34.  
  35. Set.prototype['has'] = function(value) {
  36. return (this._values.indexOf(encodeVal(value)) !== -1);
  37. }
  38.  
  39. Set.prototype['delete'] = function(value) {
  40. var idx = this._values.indexOf(encodeVal(value));
  41. if (idx == -1) return false;
  42. this._values.splice(idx, 1);
  43. --this.size;
  44. return true;
  45. }
  46.  
  47. Set.prototype['clear'] = function(value) {
  48. ...
  49. }
  50.  
  51. Set.prototype['forEach'] = function(callbackFn, thisArg) {
  52. ...
  53. }
  54.  
  55. Set.length = 0;
  56.  
  57. global.Set = Set;
  58.  
  59. })(this)

写段测试用例:

  1. let set = new Set([1, 2, 3]);
  2.  
  3. set.add(NaN);
  4. console.log(set.size); // 3
  5.  
  6. set.add(NaN);
  7. console.log(set.size); // 3

模拟实现第三版

在模拟实现 Set 时,最麻烦的莫过于迭代器的实现和处理,比如初始化以及执行 keys()、values()、entries() 方法时都会返回迭代器:

  1. let set = new Set([1, 2, 3]);
  2.  
  3. console.log([...set]); // [1, 2, 3]
  4. console.log(set.keys()); // SetIterator {1, 2, 3}
  5. console.log([...set.keys()]); // [1, 2, 3]
  6. console.log([...set.values()]); // [1, 2, 3]
  7. console.log([...set.entries()]); // [[1, 1], [2, 2], [3, 3]]

而且 Set 也支持初始化的时候传入迭代器:

  1. let set = new Set(new Set([1, 2, 3]));
  2. console.log(set.size); // 3

当初始化传入一个迭代器的时候,我们可以根据我们在上一篇 《ES6 系列之迭代器与 for of》中模拟实现的 forOf 函数,遍历传入的迭代器的 Symbol.iterator 接口,然后依次执行 add 方法。

而当执行 keys() 方法时,我们可以返回一个对象,然后为其部署 Symbol.iterator 接口,实现的代码,也是最终的代码如下:

  1. /**
  2. * 模拟实现第三版
  3. */
  4. (function(global) {
  5.  
  6. var NaNSymbol = Symbol('NaN');
  7.  
  8. var encodeVal = function(value) {
  9. return value !== value ? NaNSymbol : value;
  10. }
  11.  
  12. var decodeVal = function(value) {
  13. return (value === NaNSymbol) ? NaN : value;
  14. }
  15.  
  16. var makeIterator = function(array, iterator) {
  17. var nextIndex = 0;
  18.  
  19. // new Set(new Set()) 会调用这里
  20. var obj = {
  21. next: function() {
  22. return nextIndex < array.length ? { value: iterator(array[nextIndex++]), done: false } : { value: void 0, done: true };
  23. }
  24. };
  25.  
  26. // [...set.keys()] 会调用这里
  27. obj[Symbol.iterator] = function() {
  28. return obj
  29. }
  30.  
  31. return obj
  32. }
  33.  
  34. function forOf(obj, cb) {
  35. let iterable, result;
  36.  
  37. if (typeof obj[Symbol.iterator] !== "function") throw new TypeError(obj + " is not iterable");
  38. if (typeof cb !== "function") throw new TypeError('cb must be callable');
  39.  
  40. iterable = obj[Symbol.iterator]();
  41.  
  42. result = iterable.next();
  43. while (!result.done) {
  44. cb(result.value);
  45. result = iterable.next();
  46. }
  47. }
  48.  
  49. function Set(data) {
  50. this._values = [];
  51. this.size = 0;
  52.  
  53. forOf(data, (item) => {
  54. this.add(item);
  55. })
  56.  
  57. }
  58.  
  59. Set.prototype['add'] = function(value) {
  60. value = encodeVal(value);
  61. if (this._values.indexOf(value) == -1) {
  62. this._values.push(value);
  63. ++this.size;
  64. }
  65. return this;
  66. }
  67.  
  68. Set.prototype['has'] = function(value) {
  69. return (this._values.indexOf(encodeVal(value)) !== -1);
  70. }
  71.  
  72. Set.prototype['delete'] = function(value) {
  73. var idx = this._values.indexOf(encodeVal(value));
  74. if (idx == -1) return false;
  75. this._values.splice(idx, 1);
  76. --this.size;
  77. return true;
  78. }
  79.  
  80. Set.prototype['clear'] = function(value) {
  81. this._values = [];
  82. this.size = 0;
  83. }
  84.  
  85. Set.prototype['forEach'] = function(callbackFn, thisArg) {
  86. thisArg = thisArg || global;
  87. for (var i = 0; i < this._values.length; i++) {
  88. callbackFn.call(thisArg, this._values[i], this._values[i], this);
  89. }
  90. }
  91.  
  92. Set.prototype['values'] = Set.prototype['keys'] = function() {
  93. return makeIterator(this._values, function(value) { return decodeVal(value); });
  94. }
  95.  
  96. Set.prototype['entries'] = function() {
  97. return makeIterator(this._values, function(value) { return [decodeVal(value), decodeVal(value)]; });
  98. }
  99.  
  100. Set.prototype[Symbol.iterator] = function(){
  101. return this.values();
  102. }
  103.  
  104. Set.prototype['forEach'] = function(callbackFn, thisArg) {
  105. thisArg = thisArg || global;
  106. var iterator = this.entries();
  107.  
  108. forOf(iterator, (item) => {
  109. callbackFn.call(thisArg, item[1], item[0], this);
  110. })
  111. }
  112.  
  113. Set.length = 0;
  114.  
  115. global.Set = Set;
  116.  
  117. })(this)

写段测试代码:

  1. let set = new Set(new Set([1, 2, 3]));
  2. console.log(set.size); // 3
  3.  
  4. console.log([...set.keys()]); // [1, 2, 3]
  5. console.log([...set.values()]); // [1, 2, 3]
  6. console.log([...set.entries()]); // [1, 2, 3]

QUnit

由上我们也可以发现,每当我们进行一版的修改时,只是写了新的测试代码,但是代码改写后,对于之前的测试代码是否还能生效呢?是否不小心改了什么导致以前的测试代码没有通过呢?

为了解决这个问题,针对模拟实现 Set 这样一个简单的场景,我们可以引入 QUnit 用于编写测试用例,我们新建一个 HTML 文件:

  1. <!DOCTYPE html>
  2. <html>
  3.  
  4. <head>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width">
  7. <title>Set 的模拟实现</title>
  8. <link rel="stylesheet" href="qunit-2.4.0.css">
  9. </head>
  10.  
  11. <body>
  12. <div id="qunit"></div>
  13. <div id="qunit-fixture"></div>
  14. <script src="qunit-2.4.0.js"></script>
  15. <script src="polyfill-set.js"></script>
  16. <script src="test.js"></script>
  17. </body>
  18.  
  19. </html>

编写测试用例,因为语法比较简单,我们就直接看编写的一些例子:

  1. QUnit.test("unique value", function(assert) {
  2. const set = new Set([1, 2, 3, 4, 4]);
  3. assert.deepEqual([...set], [1, 2, 3, 4], "Passed!");
  4. });
  5.  
  6. QUnit.test("unique value", function(assert) {
  7. const set = new Set(new Set([1, 2, 3, 4, 4]));
  8. assert.deepEqual([...set], [1, 2, 3, 4], "Passed!");
  9. });
  10.  
  11. QUnit.test("NaN", function(assert) {
  12. const items = new Set([NaN, NaN]);
  13. assert.ok(items.size == 1, "Passed!");
  14. });
  15.  
  16. QUnit.test("Object", function(assert) {
  17. const items = new Set([{}, {}]);
  18. assert.ok(items.size == 2, "Passed!");
  19. });
  20.  
  21. QUnit.test("set.keys", function(assert) {
  22. let set = new Set(['red', 'green', 'blue']);
  23. assert.deepEqual([...set.keys()], ["red", "green", "blue"], "Passed!");
  24. });
  25.  
  26.  
  27. QUnit.test("set.forEach", function(assert) {
  28. let temp = [];
  29. let set = new Set([1, 2, 3]);
  30. set.forEach((value, key) => temp.push(value * 2) )
  31.  
  32. assert.deepEqual(temp, [2, 4, 6], "Passed!");
  33. });

用浏览器预览 HTML 页面,效果如下图:

Qunit截图

完整的 polyfill 及 Qunit 源码在 https://github.com/mqyqingfeng/Blog/tree/master/demos/qunit

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。