你造嗎
每當去學習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
如圖所示:
現在先來實現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
// 差集
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,那麼感謝大家的觀看了,前面都是廢話,這才是真心的,哈哈