ES6 系列之模擬實現一個 Set 資料結構

冴羽發表於2018-07-18

基本介紹

ES6 提供了新的資料結構 Set。

它類似於陣列,但是成員的值都是唯一的,沒有重複的值。

初始化

Set 本身是一個建構函式,用來生成 Set 資料結構。

let set = new Set();
複製程式碼

Set 函式可以接受一個陣列(或者具有 iterable 介面的其他資料結構)作為引數,用來初始化。

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

set = new Set(document.querySelectorAll('div'));
console.log(set.size); // 66

set = new Set(new Set([1, 2, 3, 4]));
console.log(set.size); // 4
複製程式碼

屬性和方法

操作方法有:

  1. add(value):新增某個值,返回 Set 結構本身。
  2. delete(value):刪除某個值,返回一個布林值,表示刪除是否成功。
  3. has(value):返回一個布林值,表示該值是否為 Set 的成員。
  4. clear():清除所有成員,無返回值。

舉個例子:

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

console.log(set.delete(2)); // true
console.log(set.has(2)); // false

console.log(set.clear()); // undefined
console.log(set.has(1)); // false
複製程式碼

之所以每個操作都 console 一下,就是為了讓大家注意每個操作的返回值。

遍歷方法有:

  1. keys():返回鍵名的遍歷器
  2. values():返回鍵值的遍歷器
  3. entries():返回鍵值對的遍歷器
  4. forEach():使用回撥函式遍歷每個成員,無返回值

注意 keys()、values()、entries() 返回的是遍歷器

let set = new Set(['a', 'b', 'c']);
console.log(set.keys()); // SetIterator {"a", "b", "c"}
console.log([...set.keys()]); // ["a", "b", "c"]
複製程式碼
let set = new Set(['a', 'b', 'c']);
console.log(set.values()); // SetIterator {"a", "b", "c"}
console.log([...set.values()]); // ["a", "b", "c"]
複製程式碼
let set = new Set(['a', 'b', 'c']);
console.log(set.entries()); // SetIterator {"a", "b", "c"}
console.log([...set.entries()]); // [["a", "a"], ["b", "b"], ["c", "c"]]
複製程式碼
let set = new Set([1, 2, 3]);
set.forEach((value, key) => console.log(key + ': ' + value));
// 1: 1
// 2: 2
// 3: 3
複製程式碼

屬性:

  1. Set.prototype.constructor:建構函式,預設就是 Set 函式。
  2. Set.prototype.size:返回 Set 例項的成員總數。

模擬實現第一版

如果要模擬實現一個簡單的 Set 資料結構,實現 add、delete、has、clear、forEach 方法,還是很容易寫出來的,這裡直接給出程式碼:

/**
 * 模擬實現第一版
 */
(function(global) {

    function Set(data) {
        this._values = [];
        this.size = 0;

        data && data.forEach(function(item) {
            this.add(item);
        }, this);
    }

    Set.prototype['add'] = function(value) {
        if (this._values.indexOf(value) == -1) {
            this._values.push(value);
            ++this.size;
        }
        return this;
    }

    Set.prototype['has'] = function(value) {
        return (this._values.indexOf(value) !== -1);
    }

    Set.prototype['delete'] = function(value) {
        var idx = this._values.indexOf(value);
        if (idx == -1) return false;
        this._values.splice(idx, 1);
        --this.size;
        return true;
    }

    Set.prototype['clear'] = function(value) {
        this._values = [];
        this.size = 0;
    }

    Set.prototype['forEach'] = function(callbackFn, thisArg) {
        thisArg = thisArg || global;
        for (var i = 0; i < this._values.length; i++) {
            callbackFn.call(thisArg, this._values[i], this._values[i], this);
        }
    }

    Set.length = 0;

    global.Set = Set;

})(this)
複製程式碼

我們可以寫段測試程式碼:

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

set.delete(1);
console.log(set.has(1)); // false

set.clear();
console.log(set.size); // 0

set = new Set([1, 2, 3, 4, 4]);
set.forEach((value, key, set) => {
	console.log(value, key, set.size)
});
// 1 1 4
// 2 2 4
// 3 3 4
// 4 4 4
複製程式碼

模擬實現第二版

在第一版中,我們使用 indexOf 來判斷新增的元素是否重複,本質上,還是使用 === 來進行比較,對於 NaN 而言,因為:

console.log([NaN].indexOf(NaN)); // -1
複製程式碼

模擬實現的 Set 其實可以新增多個 NaN 而不會去重,然而對於真正的 Set 資料結構:

let set = new Set();
set.add(NaN);
set.add(NaN);
console.log(set.size); // 1
複製程式碼

所以我們需要對 NaN 這個值進行單獨的處理。

處理的方式是當判斷新增的值是 NaN 時,將其替換為一個獨一無二的值,比如說一個很難重複的字串類似於 @@NaNValue,當然了,說到獨一無二的值,我們也可以直接使用 Symbol,程式碼如下:

/**
 * 模擬實現第二版
 */
(function(global) {

    var NaNSymbol = Symbol('NaN');

    var encodeVal = function(value) {
        return value !== value ? NaNSymbol : value;
    }

    var decodeVal = function(value) {
        return (value === NaNSymbol) ? NaN : value;
    }

    function Set(data) {
        this._values = [];
        this.size = 0;

        data && data.forEach(function(item) {
            this.add(item);
        }, this);

    }

    Set.prototype['add'] = function(value) {
        value = encodeVal(value);
        if (this._values.indexOf(value) == -1) {
            this._values.push(value);
            ++this.size;
        }
        return this;
    }

    Set.prototype['has'] = function(value) {
        return (this._values.indexOf(encodeVal(value)) !== -1);
    }

    Set.prototype['delete'] = function(value) {
        var idx = this._values.indexOf(encodeVal(value));
        if (idx == -1) return false;
        this._values.splice(idx, 1);
        --this.size;
        return true;
    }

    Set.prototype['clear'] = function(value) {
        ...
    }

    Set.prototype['forEach'] = function(callbackFn, thisArg) {
        ...
    }

    Set.length = 0;

    global.Set = Set;

})(this)
複製程式碼

寫段測試用例:

let set = new Set([1, 2, 3]);

set.add(NaN);
console.log(set.size); // 3

set.add(NaN);
console.log(set.size); // 3
複製程式碼

模擬實現第三版

在模擬實現 Set 時,最麻煩的莫過於迭代器的實現和處理,比如初始化以及執行 keys()、values()、entries() 方法時都會返回迭代器:

let set = new Set([1, 2, 3]);

console.log([...set]); // [1, 2, 3]
console.log(set.keys()); // SetIterator {1, 2, 3}
console.log([...set.keys()]); // [1, 2, 3]
console.log([...set.values()]); // [1, 2, 3]
console.log([...set.entries()]); // [[1, 1], [2, 2], [3, 3]]
複製程式碼

而且 Set 也支援初始化的時候傳入迭代器:

let set = new Set(new Set([1, 2, 3]));
console.log(set.size); // 3
複製程式碼

當初始化傳入一個迭代器的時候,我們可以根據我們在上一篇 《ES6 系列之迭代器與 for of》中模擬實現的 forOf 函式,遍歷傳入的迭代器的 Symbol.iterator 介面,然後依次執行 add 方法。

而當執行 keys() 方法時,我們可以返回一個物件,然後為其部署 Symbol.iterator 介面,實現的程式碼,也是最終的程式碼如下:

/**
 * 模擬實現第三版
 */
(function(global) {

    var NaNSymbol = Symbol('NaN');

    var encodeVal = function(value) {
        return value !== value ? NaNSymbol : value;
    }

    var decodeVal = function(value) {
        return (value === NaNSymbol) ? NaN : value;
    }

    var makeIterator = function(array, iterator) {
        var nextIndex = 0;

        // new Set(new Set()) 會呼叫這裡
        var obj = {
            next: function() {
                return nextIndex < array.length ? { value: iterator(array[nextIndex++]), done: false } : { value: void 0, done: true };
            }
        };

        // [...set.keys()] 會呼叫這裡
        obj[Symbol.iterator] = function() {
            return obj
        }

        return obj
    }

    function forOf(obj, cb) {
        let iterable, result;

        if (typeof obj[Symbol.iterator] !== "function") throw new TypeError(obj + " is not iterable");
        if (typeof cb !== "function") throw new TypeError('cb must be callable');

        iterable = obj[Symbol.iterator]();

        result = iterable.next();
        while (!result.done) {
            cb(result.value);
            result = iterable.next();
        }
    }

    function Set(data) {
        this._values = [];
        this.size = 0;

        forOf(data, (item) => {
            this.add(item);
        })

    }

    Set.prototype['add'] = function(value) {
        value = encodeVal(value);
        if (this._values.indexOf(value) == -1) {
            this._values.push(value);
            ++this.size;
        }
        return this;
    }

    Set.prototype['has'] = function(value) {
        return (this._values.indexOf(encodeVal(value)) !== -1);
    }

    Set.prototype['delete'] = function(value) {
        var idx = this._values.indexOf(encodeVal(value));
        if (idx == -1) return false;
        this._values.splice(idx, 1);
        --this.size;
        return true;
    }

    Set.prototype['clear'] = function(value) {
        this._values = [];
        this.size = 0;
    }

    Set.prototype['forEach'] = function(callbackFn, thisArg) {
        thisArg = thisArg || global;
        for (var i = 0; i < this._values.length; i++) {
            callbackFn.call(thisArg, this._values[i], this._values[i], this);
        }
    }

    Set.prototype['values'] = Set.prototype['keys'] = function() {
        return makeIterator(this._values, function(value) { return decodeVal(value); });
    }

    Set.prototype['entries'] = function() {
        return makeIterator(this._values, function(value) { return [decodeVal(value), decodeVal(value)]; });
    }

    Set.prototype[Symbol.iterator] = function(){
        return this.values();
    }

    Set.prototype['forEach'] = function(callbackFn, thisArg) {
        thisArg = thisArg || global;
        var iterator = this.entries();

        forOf(iterator, (item) => {
            callbackFn.call(thisArg, item[1], item[0], this);
        })
    }

    Set.length = 0;

    global.Set = Set;

})(this)
複製程式碼

寫段測試程式碼:

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

console.log([...set.keys()]); // [1, 2, 3]
console.log([...set.values()]); // [1, 2, 3]
console.log([...set.entries()]); // [1, 2, 3]
複製程式碼

QUnit

由上我們也可以發現,每當我們進行一版的修改時,只是寫了新的測試程式碼,但是程式碼改寫後,對於之前的測試程式碼是否還能生效呢?是否不小心改了什麼導致以前的測試程式碼沒有通過呢?

為了解決這個問題,針對模擬實現 Set 這樣一個簡單的場景,我們可以引入 QUnit 用於編寫測試用例,我們新建一個 HTML 檔案:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>Set 的模擬實現</title>
    <link rel="stylesheet" href="qunit-2.4.0.css">
</head>

<body>
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>
    <script src="qunit-2.4.0.js"></script>
    <script src="polyfill-set.js"></script>
    <script src="test.js"></script>
</body>

</html>
複製程式碼

編寫測試用例,因為語法比較簡單,我們就直接看編寫的一些例子:

QUnit.test("unique value", function(assert) {
    const set = new Set([1, 2, 3, 4, 4]);
    assert.deepEqual([...set], [1, 2, 3, 4], "Passed!");
});

QUnit.test("unique value", function(assert) {
    const set = new Set(new Set([1, 2, 3, 4, 4]));
    assert.deepEqual([...set], [1, 2, 3, 4], "Passed!");
});

QUnit.test("NaN", function(assert) {
    const items = new Set([NaN, NaN]);
    assert.ok(items.size == 1, "Passed!");
});

QUnit.test("Object", function(assert) {
    const items = new Set([{}, {}]);
    assert.ok(items.size == 2, "Passed!");
});

QUnit.test("set.keys", function(assert) {
    let set = new Set(['red', 'green', 'blue']);
    assert.deepEqual([...set.keys()], ["red", "green", "blue"], "Passed!");
});


QUnit.test("set.forEach", function(assert) {
    let temp = [];
    let set = new Set([1, 2, 3]);
    set.forEach((value, key) => temp.push(value * 2) )

    assert.deepEqual(temp, [2, 4, 6], "Passed!");
});
複製程式碼

用瀏覽器預覽 HTML 頁面,效果如下圖:

Qunit截圖

完整的 polyfill 及 Qunit 原始碼在 github.com/mqyqingfeng…

ES6 系列

ES6 系列目錄地址:github.com/mqyqingfeng…

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函式、Symbol、Set、Map 以及 Promise 的模擬實現、模組載入方案、非同步處理等內容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章