面试官:你对Es6很了解?那实现一个Set吧

开发 架构
es6新增了Set数据结构,它允许你存储任何类型的唯一值,无论是原始值还是对象引用。这篇文章希望通过模拟实现一个Set来增加对它的理解。

[[440012]]

本文转载自微信公众号「前端胖头鱼」,作者前端胖头鱼。转载本文请联系前端胖头鱼公众号。

前言

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] 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

基本语法

以下内容基本出自MDN,这里写出来,纯粹是为了便于后面的模拟操作。如果你已经很熟悉了,可以直接略过。

new Set([ iterable ]) 
  • 1.

可以传递一个可迭代对象,它的所有元素将被添加到新的 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) {} 
  • 1.
  • 2.
  • 3.

实例属性和方法

属性

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) {} 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

遍历方法

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, keyset) { 
  console.log(value, keyset, 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"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

整体结构

以上回顾了一下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 ] () {} 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

辅助方法

开始实现Set细节前,我们先看一下会用到的一些辅助方法

assert, 这个方法是学习vuex源码时候看到的,感觉蛮实用的,主要用来对某些条件进行判断,抛出错误。

const assert = (condition, msg) => { 
  if (!condition) throw new Error(msg) 

  • 1.
  • 2.
  • 3.

isDef, 过滤掉null和undefined

const isDef = (value) => { 
  return value != void 0 

  • 1.
  • 2.
  • 3.

isIterable, 简单判断value是否是迭代器对象.

const isIterable = (value) => { 
  return isDef(value) && typeof value[ Symbol.iterator ] === 'function' 

  • 1.
  • 2.
  • 3.

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() 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

源码实现

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() 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.

测试一把

执行 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: [] } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

上面的例子把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, keyset) { 
  console.log(value, keyset, 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"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

遍历方法看起来也可以达到和前面例子一样的效果,源码实现部分基本就到这里啦,但是还没完...

  • 为什么[ ...s2 ]可以得到数组[ 's', 'e', 't' ]呢?
  • s2 为什么可以被for of循环呢?

iterator(迭代器)

从MDN找来这段话,在JavaScript中迭代器是一个对象,它提供了一个next() 方法,用来返回序列中的下一项。这个方法返回包含两个属性:done(表示遍历是否结束)和 value(当前的值)。

迭代器对象一旦被创建,就可以反复调用next()。

function makeIterator(array){ 
  var nextIndex = 0 
 
  return { 
    nextfunction () { 
      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 } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

这个时候可以讲一下我们的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 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

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 } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

看到这里你可能已经知道了,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 

  • 1.
  • 2.
  • 3.

到这里上面的问题2就可以解释通了。

再看看问题1为什么[ ...s2 ]可以得到数组[ 's', 'e', 't' ]呢?,原因也是我们给Set、keys、values、entries部署了Symbol.iterator,使之具有“iterator”接口,而扩展运算符...的特点之一就是任何具有Iterator接口的对象,都可以用扩展运算符转为真正的数组。

结尾

模拟过程中可能会有相应的错误,也不是和原生的实现完全一致。仅当学习之用,欢迎大家拍砖。

参考

  • Set
  • 迭代器和生成器
  • ES6 系列之模拟实现一个 Set 数据结构
  • 展开语法
  •  for...of 循环

 

责任编辑:武晓燕 来源: 前端胖头鱼
相关推荐

2022-03-21 09:05:18

volatileCPUJava

2024-09-09 08:30:56

代码

2020-07-03 14:19:01

Kafka日志存储

2024-05-28 10:14:31

JavaScrip模板引擎

2024-01-15 06:42:00

高并发热点账户数据库

2022-04-08 08:26:03

JavaHTTP请求

2021-09-28 13:42:55

Chrome Devwebsocket网络协议

2020-06-19 15:32:56

HashMap面试代码

2021-11-25 10:18:42

RESTfulJava互联网

2021-08-09 07:47:40

Git面试版本

2025-01-13 09:24:32

2020-02-18 15:15:05

Handler消息队列执行任务

2019-12-25 11:22:19

负载均衡集群算法

2019-07-15 16:35:43

MySQL索引阿里

2015-08-13 10:29:12

面试面试官

2020-02-28 15:42:26

AOPJDKCGLib

2020-12-01 08:47:36

Java异常开发

2024-09-27 15:43:52

零拷贝DMAIO

2020-06-12 15:50:56

options前端服务器

2025-02-21 15:25:54

虚拟线程轻量级
点赞
收藏

51CTO技术栈公众号