從零到有模擬實現一個Set類
前言
es6新增了Set資料結構,它允許你儲存任何型別的唯一值,無論是原始值還是物件引用。這篇文章希望通過模擬實現一個Set來增加對它的理解。
用在前面
實際工作和學習過程中,你可能也經常用Set來對陣列做去重處理
let unique = (array) => {
return [ ...new Set(array) ]
}
console.log(unique([ 1, 2, 3, 4, 1, 2, 5 ])) // [1, 2, 3, 4, 5]
基本語法
以下內容基本出自MDN,這裡寫出來,純粹是為了便於後面的模擬操作。如果你已經很熟悉了,可以直接略過。
new Set([ iterable ])
可以傳遞一個可迭代物件,它的所有元素將被新增到新的 Set中。如果不指定此引數或其值為null,則新的 Set為空。
let s = new Set([ 1, 2, 3 ]) // Set(3) {1, 2, 3}
let s2 = new Set() // Set(0) {}
let s3 = new Set(null /* or undefined */) // Set(0) {}
例項屬性和方法
屬性
constructor
Set的建構函式
size
Set 長度
操作方法
- Set.prototype.add(value)
在Set物件尾部新增一個元素。返回該Set物件。
- Set.prototype.has(value)
返回一個布林值,表示該值在Set中存在與否。
- Set.prototype.delete(value)
移除Set中與這個值相等的元素,返回Set.prototype.has(value)在這個操作前會返回的值(即如果該元素存在,返回true,否則返回false)
- Set.prototype.clear()
移除Set物件內的所有元素。沒有返回值
栗子
let s = new Set() s.add(1) // Set(1) {1} .add(2) // Set(2) {1, 2} .add(NaN) // Set(2) {1, 2, NaN} .add(NaN) // Set(2) {1, 2, NaN} // 注意這裡因為新增完元素之後返回的是該Set物件,所以可以鏈式呼叫 // NaN === NaN 結果是false,但是Set中只會存一個NaN s.has(1) // true s.has(NaN) // true s.size // 3 s.delete(1) s.has(1) // false s.size // 2 s.clear() s // Set(0) {}
遍歷方法
- Set.prototype.keys()
返回一個新的迭代器物件,該物件包含Set物件中的按插入順序排列的所有元素的值。
- Set.prototype.values()
返回一個新的迭代器物件,該物件包含Set物件中的按插入順序排列的所有元素的值。
- Set.prototype.entries()
返回一個新的迭代器物件,該物件包含Set物件中的按插入順序排列的所有元素的值的[value, value]陣列。為了使這個方法和Map物件保持相似, 每個值的鍵和值相等。
- Set.prototype.forEach(callbackFn[, thisArg])
按照插入順序,為Set物件中的每一個值呼叫一次callBackFn。如果提供了thisArg引數,回撥中的this會是這個引數。
栗子
let s = new Set([ 's', 'e', 't' ])
s // SetIterator {"s", "e", "t"}
s.keys() // SetIterator {"s", "e", "t"}
s.values() // SetIterator {"s", "e", "t"}
s.entries() // SetIterator {"s", "e", "t"}
// log
[ ...s ] // ["s", "e", "t"]
[ ...s.keys() ] // ["s", "e", "t"]
[ ...s.values() ] // ["s", "e", "t"]
[ ...s.entries() ] // [["s", "s"], ["e", "e"], ["t", "t"]]
s.forEach(function (value, key, set) {
console.log(value, key, set, this)
})
// s s Set(3) {"s", "e", "t"} Window
// e e Set(3) {"s", "e", "t"} Window
// t t Set(3) {"s", "e", "t"} Window
s.forEach(function () {
console.log(this)
}, { name: 'qianlongo' })
// {name: "qianlongo"}
// {name: "qianlongo"}
// {name: "qianlongo"}
for (let value of s) {
console.log(value)
}
// s
// e
// t
for (let value of s.entries()) {
console.log(value)
}
// ["s", "s"]
// ["e", "e"]
// ["t", "t"]
整體結構
以上回顧了一下Set的基本使用,我們可以開始嘗試模擬實現一把啦。你也可以直接點選檢視原始碼。
目錄結構
├──set-polyfill│ ├──iterator.js // 匯出一個建構函式Iterator,模擬建立可迭代物件│ ├──set.js // Set類│ ├──utils.js // 輔助函式│ ├──test.js // 測試
Set整體框架
class Set {
constructor (iterable) {}
get size () {}
has () {}
add () {}
delete () {}
clear () {}
forEach () {}
keys () {}
values () {}
entries () {}
[ Symbol.iterator ] () {}
}
輔助方法
開始實現Set細節前,我們先看一下會用到的一些輔助方法
- assert, 這個方法是學習vuex原始碼時候看到的,感覺蠻實用的,主要用來對某些條件進行判斷,丟擲錯誤。
const assert = (condition, msg) => {
if (!condition) throw new Error(msg)
}
- isDef, 過濾掉
null
和undefined
const isDef = (value) => {
return value != void 0
}
- isIterable, 簡單判斷value是否是迭代器物件.
const isIterable = (value) => {
return isDef(value) && typeof value[ Symbol.iterator ] === 'function'
}
- forOf, 模擬
for of
行為, 對迭代器物件進行遍歷操作。
const forOf = (iterable, callback, ctx) => {
let result
iterable = iterable[ Symbol.iterator ]()
result = iterable.next()
while (!result.done) {
callback.call(ctx, result.value)
result = iterable.next()
}
}
原始碼實現
class Set {
constructor (iterable) {
// 使用陣列來儲存Set的每一項元素
this.value = []
// 判斷是否使用new呼叫
assert(this instanceof Set, 'Constructor Set requires "new"')
// 過濾掉null和undefined
if (isDef(iterable)) {
// 是可迭代物件才進行下一步forOf元素新增
assert(isIterable(iterable), `${iterable} is not iterable`)
// 迴圈可迭代物件,初始化
forOf(iterable, (value) => {
this.add(value)
})
}
}
// 獲取s.size時候會呼叫 size函式,返回value陣列的長度
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/get
get size () {
return this.value.length
}
// 使用陣列的includes方法判斷是否包含value
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
// [ NaN ].includes(NaN)會返回true,正好Set也只能存一個NaN
has (value) {
return this.value.includes(value)
}
// 通過has方法判斷value是否存在,不存在則新增進陣列,最後返回Set本身,支援鏈式呼叫
add (value) {
if (!this.has(value)) {
this.value.push(value)
}
return this
}
// 在刪除之前先判斷value是否存在用之當做返回值,存在則通過splice方法移除
delete (value) {
let result = this.has(value)
if (result) {
this.value.splice(this.value.indexOf(value), 1)
}
return result
}
// 重新賦值一個空陣列,即實現clear方法
clear () {
this.value = []
}
// 通過forOf遍歷 values返回的迭代物件,實現forEach
forEach (callback, thisArg) {
forOf(this.values(), (value) => {
callback.call(thisArg, value, value, this)
})
}
// 返回一個迭代物件,該物件中的值是Set中的value
keys () {
return new Iterator(this.value)
}
// 同keys
values () {
return this.keys()
}
// 返回一個迭代物件,不同keys和values的是其值是[value, value]
entries () {
return new Iterator(this.value, (value) => [ value, value ])
}
// 返回一個新的迭代器物件,該物件包含Set物件中的按插入順序排列的所有元素的值。
[ Symbol.iterator ] () {
return this.values()
}
}
測試一把
執行 node test.js
size屬性和操作方法
const Set = require('./set')
const s = new Set()
s.add(1)
.add(2)
.add(NaN)
.add(NaN)
console.log(s) // Set { value: [ 1, 2, NaN ] }
console.log(s.has(1)) // true
console.log(s.has(NaN)) // true
console.log(s.size) // 3
s.delete(1)
console.log(s.has(1)) // false
console.log(s.size) // 2
s.clear()
console.log(s) // Set { value: [] }
上面的例子把Set的size屬性和操作方法過了一遍,打印出來的Set例項和原生的長得不太一樣,就先不管了。
遍歷方法
let s2 = new Set([ 's', 'e', 't' ])
console.log(s2) // Set { value: [ 's', 'e', 't' ] }
console.log(s2.keys()) // Iterator {}
console.log(s2.values()) // Iterator {}
console.log(s2.entries()) // Iterator {}
console.log([ ...s2 ]) // [ 's', 'e', 't' ]
console.log([ ...s2.keys() ]) // [ 's', 'e', 't' ]
console.log([ ...s2.values() ]) // [ 's', 'e', 't' ]
console.log([ ...s2.entries() ]) // [ [ 's', 's' ], [ 'e', 'e' ], [ 't', 't' ] ]
s2.forEach(function (value, key, set) {
console.log(value, key, set, this)
})
// s s Set { value: [ 's', 'e', 't' ] } global
// e e Set { value: [ 's', 'e', 't' ] } global
// t t Set { value: [ 's', 'e', 't' ] } global
s2.forEach(function () {
console.log(this)
}, { name: 'qianlongo' })
// { name: 'qianlongo' }
// { name: 'qianlongo' }
// { name: 'qianlongo' }
// {name: "qianlongo"}
// {name: "qianlongo"}
// {name: "qianlongo"}
for (let value of s) {
console.log(value)
}
// s
// e
// t
for (let value of s.entries()) {
console.log(value)
}
// ["s", "s"]
// ["e", "e"]
// ["t", "t"]
遍歷方法看起來也可以達到和前面例子一樣的效果,原始碼實現部分基本就到這裡啦,但是還沒完...
- 為什麼
[ ...s2 ]
可以得到陣列[ 's', 'e', 't' ]
呢? -
s2
為什麼可以被for of
迴圈呢?
iterator(迭代器)
從MDN找來這段話,在JavaScript中迭代器是一個物件,它提供了一個next() 方法,用來返回序列中的下一項。這個方法返回包含兩個屬性:done(表示遍歷是否結束)和 value(當前的值)。
迭代器物件一旦被建立,就可以反覆呼叫next()。
function makeIterator(array){
var nextIndex = 0
return {
next: function () {
return nextIndex < array.length ?
{ done: false, value: array[ nextIndex++ ] } :
{ done: true, value: undefined }
}
};
}
var it = makeIterator(['yo', 'ya'])
console.log(it.next()) // { done: false, value: "yo" }
console.log(it.next()) // { done: false, value: "ya" }
console.log(it.next()) // { done: true, value: undefined }
這個時候可以講一下我們的iterator.js
中的程式碼了
class Iterator {
constructor (arrayLike, iteratee = (value) => value) {
this.value = Array.from(arrayLike)
this.nextIndex = 0
this.len = this.value.length
this.iteratee = iteratee
}
next () {
let done = this.nextIndex >= this.len
let value = done ? undefined : this.iteratee(this.value[ this.nextIndex++ ])
return { done, value }
}
[ Symbol.iterator ] () {
return this
}
}
Iterator
的例項有一個next方法,每次呼叫都會返回一個done
屬性和value
屬性,其語意和前面的解釋是一樣的。
let it = new Iterator(['yo', 'ya'])
console.log(it.next()) // { done: false, value: "yo" }
console.log(it.next()) // { done: false, value: "ya" }
console.log(it.next()) // { done: true, value: undefined }
看到這裡你可能已經知道了,Iterator要實現的功能之一就是提供一個迭代器。那這個又和上面的問題1和2有啥關係呢?我們再來看看for of
for of
一個數據結構只要部署了Symbol.iterator屬性,就被視為具有iterator介面,就可以用for...of迴圈遍歷它的成員。也就是說,for...of迴圈內部呼叫的是資料結構的Symbol.iterator方法 for...of 迴圈
預設只有(Array,Map,Set,String,TypedArray,arguments)可被for of
迭代。我們自定義的Set
類不在這其中,前面的例子中卻在for of
迴圈中打印出了想要的值。原因就是我們給Iterator
類部署了Symbol.iterator
方法,執行該方法便返回Iterator
例項本身,它是一個可以被迭代的物件。
[ Symbol.iterator ] () {
return this
}
到這裡上面的問題2就可以解釋通了。
再看看問題1 為什麼
[ ...s2 ]可以得到陣列
[ 's', 'e', 't' ]呢?
,原因也是我們給Set
、keys
、values
、entries
部署了Symbol.iterator,使之具有“iterator”介面,而擴充套件運算子...
的特點之一就是任何具有Iterator介面的物件,都可以用擴充套件運算子轉為真正的陣列。
結尾
模擬過程中可能會有相應的錯誤,也不是和原生的實現完全一致。僅當學習之用,歡迎大家拍磚。