ES6的Set和Map資料結構,由你製造

chenhongdong發表於2018-04-11

你造嗎

每當去學習ES6的時候,我都會不由自主的開啟阮一峰老師的《ES6入門》去學習和查詢用法,相信大多數同學也都和我一樣看過阮老師寫的文章。

當然大家也都知道ES6裡常用的API就那麼些,不多不少,用在專案中也是剛剛好。

不過在每次讀到Set和Map資料結構那一章的時候,總是有點不知所措,因為我不明白實現這樣的資料結構,目的是什麼,意義又是什麼呢

Set和Map主要的應用場景在於陣列去重資料儲存,幸運的是在讀了關於資料結構和演算法之類的書籍後,恍然大悟的發現

原來Set是一種叫做集合的資料結構,Map是一種叫做字典的資料結構

那麼下面就跟隨我一起去了解下這兩種資料結構,最後來親手實現的一個ES6中的Set和Map吧

集合

  • 集合是由一組無序且唯一(即不能重複)的項組成的,可以想象成集合是一個既沒有重複元素,也沒有順序概念的陣列
  • ES6提供了新的資料結構Set。它類似於陣列,但是成員的值都是唯一的,沒有重複的值
  • Set 本身是一個建構函式,用來生成 Set 資料結構
  • 這裡說的Set其實就是我們所要講到的集合,先來看下基礎用法
const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);   // 2 3 5 4
}

// 去除陣列的重複成員
let array = [1,2,1,4,5,3];
[...new Set(array)]     // [1, 2, 4, 5, 3]
複製程式碼

具體用法如果還有不清楚的,這裡我會在後面一一細說。現在還是來看一下以ES6中Set類(資料結構)為基礎實現的集合吧

Set例項的屬性和方法

  • Set的屬性:
    • size:返回集合所包含元素的數量
  • Set的方法:
    • 操作方法
      • add(value):向集合新增一個新的項
      • delete(value):從集合中移除一個值
      • has(value):如果值在集合中存在,返回true,否則false
      • clear(): 移除集合裡所有的項
    • 遍歷方法
      • keys():返回一個包含集合中所有鍵的陣列
      • values():返回一個包含集合中所有值的陣列
      • entries:返回一個包含集合中所有鍵值對的陣列(感覺沒什麼用就不實現了)
      • forEach():用於對集合成員執行某種操作,沒有返回值

建立一個集合

function Set(arr = []) {    // 可以傳入陣列
    let items = {};
    this.size = 0;  // 記錄集合中成員的數量
}

module.exports = Set;
複製程式碼

這裡用{}物件來表示集合,也是因為物件不允許一個鍵指向兩個不同的屬性,保證了集合裡的元素都是唯一的

接下來,就需要按照ES6中Set類的實現,新增一些集合的操作方法

has方法

首先要實現的是has方法,因為在add和delete等其他方法中都會被呼叫,下面來看一下它的實現

function Set() {
    let items = {};
    this.size = 0;
    
    // has(val)方法
    this.has = function(val) {
        // 物件都有hasOwnProperty方法,判斷是否擁有特定屬性
        return items.hasOwnProperty(val);  
    };
}
複製程式碼

add方法

接下來要實現add方法

    // add(val)方法
    this.add = function(val) {
        if (!this.has(val)) {
            items[val] = val;
            this.size++;    // 累加集合成員數量
            return true;
        }
        return false;
    };
複製程式碼

對於給定的val,可以檢測是否存在於集合中

  • 如果不存在,就新增到集合中,返回true
  • 如果存在,就直接返回false,不做任何操作

delete和clear方法

繼續寫著,這回把兩個都寫上

    // delete(val)方法
    this.delete = function(val) {
        if (this.has(val)) {
            delete items[val];  // 將items物件上的屬性刪掉
            this.size--;
            return true;
        }
        return false;
    };
    // clear方法
    this.clear = function() {
        items = {};     // 直接將集合賦一個空物件即可
        this.size = 0;
    };
複製程式碼

在delete方法中,判斷val是否存在於集合中,如果存在就直接從集合中刪掉,返回true

以上完成的都是操作方法,下面我們再來實現一下遍歷方法

keys、values方法

這兩個方法我們可以放在一起來實現,因為通過ES6對Object的擴充套件可以輕鬆實現對應的方法,下面看一下具體實現,上程式碼:

    // keys()方法
    this.keys = function() {
        return Object.keys(items);  // 返回遍歷集合的所有鍵名的陣列
    };
    // values()方法
    this.values = function() {
        return Object.values(items);  // 返回遍歷集合的所有鍵值的陣列
    };
複製程式碼

使用一下看看

// set.js
const Set = require('./Set.js');    // 匯入寫好的Set類
let set = new Set();
set.add(1);
set.add(3);
set.add(2);
console.log(set.keys());    // [ '1', '2', '3' ]
console.log(set.values());  // [ 1, 2, 3 ]
複製程式碼

這裡我們看到和ES6中的Set有點區別,因為Object的這幾個方法都是按照數值大小,從小到大遍歷的陣列,所以大家知道這一點比較好,具體實現還是有些不同的,哈哈

forEach方法

ES6中Set結構的例項上帶的forEach方法,其實和陣列的forEach方法很相似,只不過Set結構的鍵名就是鍵值,所以第一個引數與第二個引數的值永遠都是一樣的

下面就按照實現陣列的forEach方法,我們來完成Set的forEach方法

    // forEach(fn, context)方法
    this.forEach = function(fn, context = this) {
        for (let i = 0; i < this.size; i++) {
            let item = Object.keys(items)[i];
            fn.call(context, item, item, items);     
        }
    };
複製程式碼

使用forEach方法

// set.js
const Set = require('./Set.js');
let set = new Set();
set.add(1);
set.add(4);
set.add('3');
set.forEach((value, key) => console.log(key + ' : ' + value));  // 1:1, 3:3, 4:4
let arr = set.values();     // [ 1, 3, 4 ]
arr = new Set(arr.map(x => x * 2)).values();
console.log(arr);           // [ 2, 6, 8 ]
複製程式碼

基本上實現了Set結構的方法,不過,發現一個問題,那就是每次新增一個元素都要add這樣寫起來確實好麻煩,Set是可以接收一個陣列作為引數的,那麼我們把這個也實現一下

function Set(arr = []) {    // 傳入接受的陣列,如果沒有傳指定一個空陣列做為初始值
    let items = {};
    this.size = 0;
    // has方法
    this.has = function (val) {
        return items.hasOwnProperty(val);
    };
    // add方法
    this.add = function (val) {
        // 如果沒有存在items裡面就可以直接寫入
        if (!this.has(val)) {
            items[val] = val;
            this.size++;
            return true;
        }
        return false;
    };
    arr.forEach((val, i) => {   // 遍歷傳入的陣列
        this.add(val);          // 將陣列裡每一項值新增到集合中
    });
    // 省略...
}
複製程式碼

再來看看現在能不能支援傳入的陣列了

// 間接使用map和filter
const Set = require('./Set.js');
let arr = new Set([1, 2, 3]).values();
m = new Set(arr.map(x => x * 2));
f = new Set(arr.filter(x => x>1));
console.log(m.values());    // [ 2, 4, 6 ]
console.log(f.values());    // [ 2, 3 ]

// 陣列去重
let arr2 = new Set([3, 5, 2, 1, 2, 5, 5]).values();
console.log(arr2);  // [ 1, 2, 3, 5 ]
複製程式碼

現在我們有了一個和ES6中非常類似的Set類實現。如前所述,也可以用陣列替代物件,儲存元素。喜歡動手的同學們,之後也可以去嘗試一下

除此之外,Set還可以實現並集(union),交集(intersect),差集(difference)

做事還是要做全套的,我們也一一來實現一下吧

union並集和intersect交集

  • 並集的數學概念,集合A和集合B的並集,表示為A∪B
  • 交集的數學概念,集合A和集合B的交集,表示為A∩B

如圖所示:

ES6的Set和Map資料結構,由你製造
現在先來實現union方法

    // 並集
    this.union = function (other) {
        let union = new Set();
        let values = this.values();
        for (let i = 0; i < values.length; i++) {
            union.add(values[i]);
        }
        values = other.values();    // 將values重新賦值為新的集合
        for (let i = 0; i < values.length; i++) {
            union.add(values[i]);
        }

        return union;
    };
    // 交集
    this.intersect = function (other) {
        let intersect = new Set();
        let values = this.values();
        for (let i = 0; i < values.length; i++) {
            if (other.has(values[i])) {     // 檢視是否也存在於other中
                intersect.add(values[i]);   // 存在的話就像intersect中新增元素
            }
        }
        return intersect;
    };
複製程式碼

再來看下difference差集的實現,之後一起再測試一番

difference差集

  • 差集的數學概念,集合A和集合B的差集,表示為A-B

ES6的Set和Map資料結構,由你製造

    // 差集
    this.difference = function (other) {
        let difference = new Set();
        let values = this.values();
        for (let i = 0; i < values.length; i++) {
            if (!other.has(values[i])) {    // 將不存在於other集合中的新增到新的集合中
                difference.add(values[i]);
            }
        }
        return difference;
    };
複製程式碼

Set完整實現

在此,先給大家貼一下完整的實現程式碼

function Set(arr = []) {
    let items = {};
    this.size = 0;
    // has方法
    this.has = function (val) {
        return items.hasOwnProperty(val);
    };
    // add方法
    this.add = function (val) {
        // 如果沒有存在items裡面就可以直接寫入
        if (!this.has(val)) {
            items[val] = val;
            this.size++;
            return true;
        }
        return false;
    };
    arr.forEach((val, i) => {
        this.add(val);
    });
    // delete方法
    this.delete = function (val) {
        if (this.has(val)) {
            delete items[val];  // 將items物件上的屬性刪掉
            this.size--;
            return true;
        }
        return false;
    };
    // clear方法
    this.clear = function () {
        items = {};
        this.size = 0;
    };
    // keys方法
    this.keys = function () {
        return Object.keys(items);
    };
    // values方法
    this.values = function () {
        return Object.values(items);
    }
    // forEach方法
    this.forEach = function (fn, context = this) {
        for (let i = 0; i < this.size; i++) {
            let item = Object.keys(items)[i];
            fn.call(context, item, item, items);
        }
    }

    // 並集
    this.union = function (other) {
        let union = new Set();
        let values = this.values();

        for (let i = 0; i < values.length; i++) {
            union.add(values[i]);
        }
        values = other.values();    // 將values重新賦值為新的集合
        for (let i = 0; i < values.length; i++) {
            union.add(values[i]);
        }

        return union;
    };
    // 交集
    this.intersect = function (other) {
        let intersect = new Set();
        let values = this.values();
        for (let i = 0; i < values.length; i++) {
            if (other.has(values[i])) {
                intersect.add(values[i]);
            }
        }
        return intersect;
    };
    // 差集
    this.difference = function (other) {
        let difference = new Set();
        let values = this.values();
        for (let i = 0; i < values.length; i++) {
            if (!other.has(values[i])) {
                difference.add(values[i]);
            }
        }
        return difference;
    };
    // 子集
    this.subset = function(other) {
        if (this.size > other.size) {
            return false;
        } else {
            let values = this.values();
            for (let i = 0; i < values.length; i++) {
                console.log(values[i])
                console.log(other.values())
                if (!other.has(values[i])) {
                    return false;
                }
            }
            return true;
        }
    };
}

module.exports = Set;
複製程式碼

寫了辣麼多一起來測試一下吧

const Set = require('./Set.js');
let set = new Set([2, 1, 3]);
console.log(set.keys());    // [ '1', '2', '3' ]
console.log(set.values());  // [ 1, 2, 3 ]
console.log(set.size);      // 3
set.delete(1);
console.log(set.values());  // [ 2, 3 ]
set.clear();
console.log(set.size);      // 0

// 並集
let a = [1, 2, 3];
let b = new Set([4, 3, 2]);
let union = new Set(a).union(b).values();
console.log(union);     // [ 1, 2, 3, 4 ]

// 交集
let c = new Set([4, 3, 2]);
let intersect = new Set([1,2,3]).intersect(c).values();
console.log(intersect); // [ 2, 3 ]

// 差集
let d = new Set([4, 3, 2]);
let difference = new Set([1,2,3]).difference(d).values();
// [1,2,3]和[4,3,2]的差集是1
console.log(difference);    // [ 1 ]
複製程式碼

目前為止我們用集合這種資料結構就實現了類似ES6中Set類,上面的使用方法也基本一樣,大家可以之後有時間的話動手去敲一敲看一看,走過路過不能錯過

既然我們已經完成了Set的實現,那麼好事要成雙,一鼓作氣再把Map也一起寫出來,天了擼的,開始

字典

在資料結構還有一種結構叫做字典,它就是實現基於ES6中的Map類的結構

那麼集合又和字典有什麼區別呢:

  • 共同點:集合、字典可以儲存不重複的值
  • 不同點:集合是以[值,值]的形式儲存元素,字典是以[鍵,值]的形式儲存

所以這一下讓我們明白了,Map其實的主要用途也是用於儲存資料的,相比於Object只提供“字串—值”的對應,Map提供了“值—值”的對應。也就是說如果你需要“鍵值對”的資料結構,Map比Object更合適

下面來看一下基本使用:

const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false
複製程式碼

以上是Map的基本使用,還有更多有用的方法稍後會隨著實現的深入分別展示

Map的屬性和方法

屬性:

  • size:返回字典所包含的元素個數

操作方法:

  • set(key, val): 向字典中新增新元素
  • get(key):通過鍵值查詢特定的數值並返回
  • has(key):如果鍵存在字典中返回true,否則false
  • delete(key): 通過鍵值從字典中移除對應的資料
  • clear():將這個字典中的所有元素刪除

遍歷方法:

  • keys():將字典中包含的所有鍵名以陣列形式返回
  • values():將字典中包含的所有數值以陣列形式返回
  • forEach():遍歷字典的所有成員

知道了都有哪些屬性和方法,那就閒言少敘,開始建立一個字典吧

建立一個字典

function Map() {
    let items = {};
}

module.exports = Map;   // 匯出
複製程式碼

建立好了字典這個骨架,那就開始新增一些方法了

has方法

首當其衝的當然是has了,因為在set和get裡都會用到,實現思路和之前寫的集合也很類似

function Map() {
    let items = {};
    // has(key)方法
    this.has = function(val) {
        return items.hasOwnProperty(val);
    };
}
複製程式碼

實現了has方法後,我們可以來判斷字典中是否包含該屬性了,繼續來實現其他方法

set和get方法

    // set(key, val)方法
    // set相同key時,後面宣告的會覆蓋前面
    // 如: new Map().set({}, 'a')
    this.set = function(key, val) {
        items[key] = val;   
    };
    // get(key)方法
    this.get = function(key) {
        // 判斷是否有key,如果有的話直接返回對應的值
        // 如果讀取一個未知的鍵,則返回undefined
        return this.has(key) ? items[key] : undefined;
    };
複製程式碼

set和get方法寫好了,再接著搞delete和clear方法,不廢話,看

delete和clear方法

    // delete(key)方法
    this.delete = function(key) {
        if (this.has(key)) {    // 如果有key值
            delete items[key];  // 直接刪掉items上對應的屬性
            this.size--;        // 讓size總數減1
            return true;
        }
        return false;
    };
    // clear()方法
    this.clear = function() {
        items = {};
        this.size = 0;
    };
複製程式碼

上面把屬性和操作方法都分別完成了,還剩下最後的遍歷方法了,繼續寫下去,堅持到底就是勝利,各位看官也不容易了,加油加油!!!

遍歷方法(keys,values,forEach)

    // keys()方法
    this.keys = function() {
        return Object.keys(items);
    };
    // values()方法
    this.values = function() {
        return Object.values(items);  
    };
    // forEach(fn, context)方法
    this.forEach = function(fn, context = this) {
        for (let i = 0; i < this.size; i++) {
            let key = Object.keys(items)[i];
            let value = Object.values(items)[i];
            fn.call(context, value, key, items);
        }
    };
複製程式碼

Now終於完成了Map類的實現,我給大家貼一下完整程式碼和測試用例,供大家空閒時間來研究分析分析

Map完整實現

function Map() {
    let items = {};
    this.size = 0;

    // 操作方法
    // has方法
    this.has = function(val) {
        return items.hasOwnProperty(val);
    };
    // set(key, val)方法
    this.set = function(key, val) {
        items[key] = val;
        this.size++;
    };
    // get(key)方法
    this.get = function(key) {
        return this.has(key) ? items[key] : undefined;
    };
    // delete(key)方法
    this.delete = function(key) {
        if (this.has(key)) {
            delete items[key];
            this.size--;
            return true;
        }
        return false;
    };
    // clear()方法
    this.clear = function() {
        items = {};
        this.size = 0;
    };
    // 遍歷方法
    // keys()方法
    this.keys = function() {
        return Object.keys(items);
    };
    // values()方法
    this.values = function() {
        return Object.values(items);
    };
    // forEach(fn, context)方法
    this.forEach = function(fn, context = this) {
        for (let i = 0; i < this.size; i++) {
            let key = Object.keys(items)[i];
            let value = Object.values(items)[i];
            fn.call(context, value, key, items);
        }
    };
}

module.exports = Map;
複製程式碼

再來看看下面的測試栗子

    // map.js
    // 使用Map類
    const Map = require('./Map.js');
    let m = new Map();
    m.set('Jay', 'Jay的Chou');
    m.set(true, '真的');
    console.log(m.has('Chou'));  // false
    console.log(m.size);        // 2
    console.log(m.keys());      // [ 'Jay', 'true' ]
    console.log(m.values());    // [ 'Jay的Chou', '真的' ]
    console.log(m.get('jay'));  // undefined
    
    m.delete(true);
    console.log(m.keys());      // [ 'Jay' ]
    console.log(m.values());    // [ 'Jay的Chou' ]
複製程式碼

這不是總結

最後的戰役,做個非正式的總結吧

根據集合和字典這兩種資料結構實現了類似ES6的Set和Map資料結構。

但其實完成的還不完善,還有一些方法需要大家一起再去思考一下

這裡我只是用集合和字典來給大家牽個頭,也希望大家看完後可以集思廣益的去實現一下

OK,那麼感謝大家的觀看了,前面都是廢話,這才是真心的,哈哈

相關文章