Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从零到有模拟实现一个Set类 #34

Open
qianlongo opened this issue Sep 7, 2018 · 0 comments
Open

从零到有模拟实现一个Set类 #34

qianlongo opened this issue Sep 7, 2018 · 0 comments
Labels

Comments

@qianlongo
Copy link
Owner

qianlongo commented Sep 7, 2018

前言

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 长度

操作方法

  1. Set.prototype.add(value)
    在Set对象尾部添加一个元素。返回该Set对象。

  2. Set.prototype.has(value)
    返回一个布尔值,表示该值在Set中存在与否。

  3. Set.prototype.delete(value)
    移除Set中与这个值相等的元素,返回Set.prototype.has(value)在这个操作前会返回的值(即如果该元素存在,返回true,否则返回false)

  4. 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. Set.prototype.keys()
    返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值。

  2. Set.prototype.values()
    返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值。

  3. Set.prototype.entries()
    返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值的[value, value]数组。为了使这个方法和Map对象保持相似, 每个值的键和值相等。

  4. 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细节前,我们先看一下会用到的一些辅助方法

  1. assert, 这个方法是学习vuex源码时候看到的,感觉蛮实用的,主要用来对某些条件进行判断,抛出错误。
const assert = (condition, msg) => {
  if (!condition) throw new Error(msg)
}
  1. isDef, 过滤掉nullundefined
const isDef = (value) => {
  return value != void 0
}
  1. isIterable, 简单判断value是否是迭代器对象.
const isIterable = (value) => {
  return isDef(value) && typeof value[ Symbol.iterator ] === 'function'
}
  1. 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"]

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

  1. 为什么[ ...s2 ]可以得到数组[ 's', 'e', 't' ]呢?
  2. 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' ]呢?,原因也是我们给Setkeysvaluesentries部署了Symbol.iterator,使之具有“iterator”接口,而扩展运算符...的特点之一就是任何具有Iterator接口的对象,都可以用扩展运算符转为真正的数组。

结尾

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

原文链接

参考

  1. Set
  2. 迭代器和生成器
  3. ES6 系列之模拟实现一个 Set 数据结构
  4. 展开语法
  5. for...of 循环
@qianlongo qianlongo changed the title 如何实现一个Set类 从零到有模拟一个Set类 Sep 14, 2018
@qianlongo qianlongo changed the title 从零到有模拟一个Set类 从零到有模拟实现一个Set类 Sep 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant