前言

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

从零到有模拟实现一个Set类的更多相关文章

  1. Linux——模拟实现一个简单的shell(带重定向)

    进程的相关知识是操作系统一个重要的模块.在理解进程概念同时,还需了解如何控制进程.对于进程控制,通常分成1.进程创建  (fork函数) 2.进程等待(wait系列) 3.进程替换(exec系列) 4 ...

  2. 自己动手模拟开发一个简单的Web服务器

    开篇:每当我们将开发好的ASP.NET网站部署到IIS服务器中,在浏览器正常浏览页面时,可曾想过Web服务器是怎么工作的,其原理是什么?“纸上得来终觉浅,绝知此事要躬行”,于是我们自己模拟一个简单的W ...

  3. 基于UDP协议模拟的一个TCP协议传输系统

    TCP协议以可靠性出名,这其中包括三次握手建立连接,流控制和拥塞控制等技术.详细介绍如下: 1. TCP协议将需要发送的数据分割成数据块.数据块大小是通过MSS(maximum segment siz ...

  4. 自己模拟的一个简单的web服务器

    首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器 ...

  5. day4作业模拟实现一个ATM + 购物商城程序

    作业需求: 模拟实现一个ATM + 购物商城程序 1.额度 15000或自定义: 2.实现购物商城,买东西加入 购物车,调用信用卡接口结账: 3.可以提现,手续费5%: 4.每月22号出账单,每月10 ...

  6. C# 二进制序列化(BinaryFormatter),Xml序列化(XmlSerializer),自己模拟写一个Xml序列化过程。

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  7. java模拟而一个电话本操作

    哈哈.大家平时都在使用电话本.以下使用java来模拟而一个简单的电话本吧... 首先给出联系人的抽象类 package net.itaem.po; /** * * 电话人的信息 * */ public ...

  8. C# Socket 模拟http服务器帮助类

    0x01 写在前面 0x02 Http协议 0x03 TCP/IP 0x04 看代码 0x05 总结 0x01 写在前面 由于工作中,经常需要在服务器之间,或者进程之间进行通信,分配任务等.用Sock ...

  9. cookiecutter-flask生成的框架里边自带了一个CRUDMixin类

    单元测试的必要性 之前曾经写过一篇讲单元测试的,正好最近也在实践和摸索.我似乎有种洁癖,就是我会严格遵守流程性的东西,比如测试,注释和文档等.目前就职的公司在我接手项目的时候是没有一行单元测试的,我挺 ...

随机推荐

  1. 【Azure API 管理】使用APIM进行XML内容读取时遇见的诡异错误 Expression evaluation failed. Object reference not set to an instance of an object.

    问题描述 使用APIM,在 Inbound 中对请求的Body内容进行解析.客户端请求所传递的Request Body为XML格式,需要从Request Body中解析出多个(Element)节点值, ...

  2. 树莓派4B安装 百度飞桨paddlelite 做视频检测 (一、环境安装)

    前言: 当前准备重新在树莓派4B8G 上面搭载训练模型进行识别检测,训练采用了百度飞桨的PaddleX再也不用为训练部署环境各种报错发愁了,推荐大家使用. 关于在树莓派4B上面paddlelite的文 ...

  3. 【持续更新】Git使用指南

    Tutorial from cs61B 1. 理解Git的不同视角 文件状态图 从状态视角理解git: 一个文件有4种状态, 状态转换如图所示 从存储视角理解git: 工作区:电脑里能看到的目录 暂存 ...

  4. 零基础,三个月内,找到??? java后端开发工作

    一.分析你的问题 出于尊重,先分析一下你的原问题吧,从您的问题,我提取到关键信息:"零基础"."三个月内"."找到工作",最后一个关键词&q ...

  5. Linux 磁盘inode字节数占满的问题

    查看ext系列文件系统的信息 #dumpe2fs /dev/sdc1 Inode count: 65536 inode号数量 Block count: 262144 块数量 Reserved bloc ...

  6. PHP-MVC-三层架构模拟

    1.控制器定义 <?php class VideoController{ public function index(){ echo "这是视频控制器的index方法"; i ...

  7. 写给开发人员的实用密码学(七)—— 非对称密钥加密算法 RSA/ECC

    本文部分内容翻译自 Practical-Cryptography-for-Developers-Book,笔者补充了密码学历史以及 openssl 命令示例,并重写了 RSA/ECC 算法原理.代码示 ...

  8. Linux----虚拟机克隆、快照、删除、

    克隆 已经安装一台linux系统 还想要更多的,直接克隆CentOS即可 使用vm ware 的克隆操作 注意: 使用前先关闭目前已开启的虚拟机 快照 作用: 虚拟系统出现异常,需要回到原先的状态,此 ...

  9. Linux 显示文件大小的命令

    ll显示的是字节,可以使用-h参数来提高文件大小的可读性,另外ll不是命令,是ls -l的别名ls -al 是以字节单位显示文件或者文件夹大小: 字节b,千字节kb, 1G=1024M=1024*10 ...

  10. 4月18日 python学习总结 异常处理、网络编程

    一. 异常 1.什么是异常 异常是错误发生的信号,程序一旦出错,如果程序中还没有相应的处理机制 那么该错误就会产生一个异常抛出来,程序的运行也随之终止 2.一个异常分为三部分: 1.异常的追踪信息 2 ...