• 从零到有模拟实现一个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 长度

    操作方法

    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 循环
  • 相关阅读:
    操作系统设计与实现(二)
    SpringCloud(八)Consul的微服务注册
    图的实现(邻接矩阵)及DFS、BFS
    SpringCloud(七)服务注册之Consul的简介和原理
    Mybatis笔记目录(6天)
    Mybatis学习笔记——day02
    C语言教程Day01
    Linux C/C++方向开发(13周学习路线)
    基于Java的实验室预约管理系统
    基于Android的高校学生考勤系统的设计与实现
  • 原文地址:https://www.cnblogs.com/10manongit/p/12760094.html
Copyright © 2020-2023  润新知