面試官:你對Es6很了解?那實現(xiàn)一個Set吧
本文轉(zhuǎn)載自微信公眾號「前端胖頭魚」,作者前端胖頭魚。轉(zhuǎn)載本文請聯(lián)系前端胖頭魚公眾號。
前言
es6新增了Set數(shù)據(jù)結(jié)構(gòu),它允許你存儲任何類型的唯一值,無論是原始值還是對象引用。這篇文章希望通過模擬實現(xiàn)一個Set來增加對它的理解。
用在前面
實際工作和學習過程中,你可能也經(jīng)常用Set來對數(shù)組做去重處理
- let unique = (array) => {
- return [ ...new Set(array) ]
- }
- console.log(unique([ 1, 2, 3, 4, 1, 2, 5 ])) // [1, 2, 3, 4, 5]
基本語法
以下內(nèi)容基本出自MDN,這里寫出來,純粹是為了便于后面的模擬操作。如果你已經(jīng)很熟悉了,可以直接略過。
- new Set([ iterable ])
可以傳遞一個可迭代對象,它的所有元素將被添加到新的 Set中。如果不指定此參數(shù)或其值為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的構(gòu)造函數(shù)
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對象內(nèi)的所有元素。沒有返回值
栗子
- 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對象,所以可以鏈式調(diào)用
- // NaN === NaN 結(jié)果是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]數(shù)組。為了使這個方法和Map對象保持相似, 每個值的鍵和值相等。
Set.prototype.forEach(callbackFn[, thisArg]) 按照插入順序,為Set對象中的每一個值調(diào)用一次callBackFn。如果提供了thisArg參數(shù),回調(diào)中的this會是這個參數(shù)。
栗子
- 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"]
整體結(jié)構(gòu)
以上回顧了一下Set的基本使用,我們可以開始嘗試模擬實現(xiàn)一把啦。你也可以直接點擊查看源碼。
目錄結(jié)構(gòu)
├──set-polyfill │ ├──iterator.js // 導出一個構(gòu)造函數(shù)Iterator,模擬創(chuàng)建可迭代對象 │ ├──set.js // Set類 │ ├──utils.js // 輔助函數(shù) │ ├──test.js // 測試
Set整體框架
- class Set {
- constructor (iterable) {}
- get size () {}
- has () {}
- add () {}
- delete () {}
- clear () {}
- forEach () {}
- keys () {}
- values () {}
- entries () {}
- [ Symbol.iterator ] () {}
- }
輔助方法
開始實現(xiàn)Set細節(jié)前,我們先看一下會用到的一些輔助方法
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()
- }
- }
源碼實現(xiàn)
- class Set {
- constructor (iterable) {
- // 使用數(shù)組來存儲Set的每一項元素
- this.value = []
- // 判斷是否使用new調(diào)用
- assert(this instanceof Set, 'Constructor Set requires "new"')
- // 過濾掉null和undefined
- if (isDef(iterable)) {
- // 是可迭代對象才進行下一步forOf元素添加
- assert(isIterable(iterable), `${iterable} is not iterable`)
- // 循環(huán)可迭代對象,初始化
- forOf(iterable, (value) => {
- this.add(value)
- })
- }
- }
- // 獲取s.size時候會調(diào)用 size函數(shù),返回value數(shù)組的長度
- // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/get
- get size () {
- return this.value.length
- }
- // 使用數(shù)組的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是否存在,不存在則添加進數(shù)組,最后返回Set本身,支持鏈式調(diào)用
- 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
- }
- // 重新賦值一個空數(shù)組,即實現(xiàn)clear方法
- clear () {
- this.value = []
- }
- // 通過forOf遍歷 values返回的迭代對象,實現(xiàn)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()
- }
- }
測試一把
執(zhí)行 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"]
遍歷方法看起來也可以達到和前面例子一樣的效果,源碼實現(xiàn)部分基本就到這里啦,但是還沒完...
- 為什么[ ...s2 ]可以得到數(shù)組[ 's', 'e', 't' ]呢?
- s2 為什么可以被for of循環(huán)呢?
iterator(迭代器)
從MDN找來這段話,在JavaScript中迭代器是一個對象,它提供了一個next() 方法,用來返回序列中的下一項。這個方法返回包含兩個屬性:done(表示遍歷是否結(jié)束)和 value(當前的值)。
迭代器對象一旦被創(chuàng)建,就可以反復調(diào)用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方法,每次調(diào)用都會返回一個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 }
看到這里你可能已經(jīng)知道了,Iterator要實現(xiàn)的功能之一就是提供一個迭代器。那這個又和上面的問題1和2有啥關(guān)系呢?我們再來看看for of
for of
一個數(shù)據(jù)結(jié)構(gòu)只要部署了Symbol.iterator屬性,就被視為具有iterator接口,就可以用for...of循環(huán)遍歷它的成員。也就是說,for...of循環(huán)內(nèi)部調(diào)用的是數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator方法 for...of 循環(huán)
默認只有(Array,Map,Set,String,TypedArray,arguments)可被for of迭代。我們自定義的Set類不在這其中,前面的例子中卻在for of循環(huán)中打印出了想要的值。原因就是我們給Iterator類部署了Symbol.iterator方法,執(zhí)行該方法便返回Iterator實例本身,它是一個可以被迭代的對象。
- [ Symbol.iterator ] () {
- return this
- }
到這里上面的問題2就可以解釋通了。
再看看問題1為什么[ ...s2 ]可以得到數(shù)組[ 's', 'e', 't' ]呢?,原因也是我們給Set、keys、values、entries部署了Symbol.iterator,使之具有“iterator”接口,而擴展運算符...的特點之一就是任何具有Iterator接口的對象,都可以用擴展運算符轉(zhuǎn)為真正的數(shù)組。
結(jié)尾
模擬過程中可能會有相應的錯誤,也不是和原生的實現(xiàn)完全一致。僅當學習之用,歡迎大家拍磚。
參考
- Set
- 迭代器和生成器
- ES6 系列之模擬實現(xiàn)一個 Set 數(shù)據(jù)結(jié)構(gòu)
- 展開語法
- for...of 循環(huán)