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

2018-09-15 admin

前言

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对象。

  1. Set.prototype.has(value)

返回一个布尔值,表示该值在Set中存在与否。

  1. Set.prototype.delete(value)

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

  1. 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对象中的按插入顺序排列的所有元素的值。

  1. Set.prototype.values()

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

  1. Set.prototype.entries()

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

  1. 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 循环

原文链接:https://segmentfault.com/a/1190000016406045

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-40295.html

文章标题:从零到有模拟实现一个Set类

相关文章
必须记住的 30 类 CSS 选择器
开篇 有 30 个 CSS 选择器你必须烂熟于心,它们适应于当今各大主流浏览器。 1.* * { margin: 0; padding: 0; } *选择器选择的是每一个单一元素。很多程序员用上面的 CSS 将所有元素的 ma...
2015-11-16
可以从CSS框架中借鉴到什么
现在很多人会使用 CSS 框架进行快速建站。   那 CSS 框架是什么呢,它通常是一些 CSS 文件的集合,这些文件包括基本布局、表单样式、网格、简单组件、以及样式重置。使用 CSS 框架大大降低工作成本进行快速建站。   当然对于一些大...
2016-03-11
javaScript+turn.js实现图书翻页效果实例代码
为了实现图书翻页的效果我们在网上可以看到很多教程 在这里推荐turn.js 网上的turn.js 有api 不过是英文的  很多人看起来不方便 .关于代码也是奇形怪状在这里我将详细讲解如何使用turn.js实现翻页效果 ,本篇文章只是讲解 ...
2017-03-16
js实现手机拍照上传功能
在前段时间的项目开发中,用到了拍照上传的地方,后来发现了最为简单的一种方法,现总结如下: &lt;form id=&quot;form&quot; method=&quot;post&quot; action=&quot;http:&#x2...
2017-03-06
纯JS实现旋转图片3D展示效果
CSS: &lt;style type=&quot;text&#x2F;css&quot;&gt; #show{position:relative;margin:20px auto;width:800px;} .item{position:...
2017-03-22
从 Node.js 分裂出 Io.js 事件看开源软件谁做主
Node.js 作为服务器编程语言的后起之秀,常用来构建和运行 Web 应用,近日却爆出其社区出现分裂。由于对官方运营商 Joyent 公司在 Node.js 管理上的长期不满,多位核心开发者另立门户,创建了分支 Io.js。从 GitHu...
2015-11-12
vue.js实现请求数据的方法示例
vue2.0示例代码如下: var vm = new Vue({ el:&quot;#list&quot;, data:{ gridData: &quot;&quot;, }, ...
2017-03-20
从 JavaScript 到 TypeScript - 声明类型
从 JavaScript 语法改写为 TypeScript 语法,有两个关键点,一点是类成员变量 (Field) 需要声明,另一点是要为各种东西 (变量、参数、函数 / 方法等) 声明类型。而这两个点直接引出了两个关键性的问题,有哪些类型?...
2017-06-05
HTML5会是下一个风口吗?
2014年10月底, W3C(万维网联盟)正式宣布HTML5正式定稿,科技圈就像发现了可以打破谷歌、苹果所统领的原生APP世界的方法,发表了很多宣讲HTML5将真正开始颠覆原生(Native)App的文章,也开始着力发展HTML5,开始抢占...
2015-11-12
何为闭包?有关JS闭包的一些理解
简单一点的说:闭包就是能够读取其他函数内部变量的函数。那如何实现读取其它函数内部变量呢,大家都知道在JavaScript中内部函数可以访问其父函数中的变量,那如果将内部函数返回是不是代表能够通过它访问其父函数中的变量了呢,闭包的原理事实上就...
2015-11-11
回到顶部