从零到有模拟实现一个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 长度
操作方法
- 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) {}
遍历方法
- 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, 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细节前,我们先看一下会用到的一些辅助方法
- assert, 这个方法是学习vuex源码时候看到的,感觉蛮实用的,主要用来对某些条件进行判断,抛出错误。
const assert = (condition, msg) => {
if (!condition) throw new Error(msg)
}
- isDef, 过滤掉
null
和undefined
const isDef = (value) => {
return value != void 0
}
- isIterable, 简单判断value是否是迭代器对象.
const isIterable = (value) => {
return isDef(value) && typeof value[ Symbol.iterator ] === 'function'
}
- 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"]
遍历方法看起来也可以达到和前面例子一样的效果,源码实现部分基本就到这里啦,但是还没完...
- 为什么
[ ...s2 ]
可以得到数组[ 's', 'e', 't' ]
呢? 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' ]呢?
,原因也是我们给Set
、keys
、values
、entries
部署了Symbol.iterator,使之具有“iterator”接口,而扩展运算符...
的特点之一就是任何具有Iterator接口的对象,都可以用扩展运算符转为真正的数组。
结尾
模拟过程中可能会有相应的错误,也不是和原生的实现完全一致。仅当学习之用,欢迎大家拍砖。
参考
从零到有模拟实现一个Set类的更多相关文章
- Linux——模拟实现一个简单的shell(带重定向)
进程的相关知识是操作系统一个重要的模块.在理解进程概念同时,还需了解如何控制进程.对于进程控制,通常分成1.进程创建 (fork函数) 2.进程等待(wait系列) 3.进程替换(exec系列) 4 ...
- 自己动手模拟开发一个简单的Web服务器
开篇:每当我们将开发好的ASP.NET网站部署到IIS服务器中,在浏览器正常浏览页面时,可曾想过Web服务器是怎么工作的,其原理是什么?“纸上得来终觉浅,绝知此事要躬行”,于是我们自己模拟一个简单的W ...
- 基于UDP协议模拟的一个TCP协议传输系统
TCP协议以可靠性出名,这其中包括三次握手建立连接,流控制和拥塞控制等技术.详细介绍如下: 1. TCP协议将需要发送的数据分割成数据块.数据块大小是通过MSS(maximum segment siz ...
- 自己模拟的一个简单的web服务器
首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器 ...
- day4作业模拟实现一个ATM + 购物商城程序
作业需求: 模拟实现一个ATM + 购物商城程序 1.额度 15000或自定义: 2.实现购物商城,买东西加入 购物车,调用信用卡接口结账: 3.可以提现,手续费5%: 4.每月22号出账单,每月10 ...
- C# 二进制序列化(BinaryFormatter),Xml序列化(XmlSerializer),自己模拟写一个Xml序列化过程。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...
- java模拟而一个电话本操作
哈哈.大家平时都在使用电话本.以下使用java来模拟而一个简单的电话本吧... 首先给出联系人的抽象类 package net.itaem.po; /** * * 电话人的信息 * */ public ...
- C# Socket 模拟http服务器帮助类
0x01 写在前面 0x02 Http协议 0x03 TCP/IP 0x04 看代码 0x05 总结 0x01 写在前面 由于工作中,经常需要在服务器之间,或者进程之间进行通信,分配任务等.用Sock ...
- cookiecutter-flask生成的框架里边自带了一个CRUDMixin类
单元测试的必要性 之前曾经写过一篇讲单元测试的,正好最近也在实践和摸索.我似乎有种洁癖,就是我会严格遵守流程性的东西,比如测试,注释和文档等.目前就职的公司在我接手项目的时候是没有一行单元测试的,我挺 ...
随机推荐
- (第一章第二部分)TensorFlow框架之图与TensorBoard
系列博客链接: (一)TensorFlow框架介绍:https://www.cnblogs.com/kongweisi/p/11038395.html 本文概述: 说明图的基本使用 应用tf.Grap ...
- (第三章)TF框架之实现验证码识别
这里实现一个用神经网络(卷积神经网络也可以)实现验证码识别的小案例,主要记录本人做这个案例的流程,不会像之前那么详细,主要用作个人记录用... 这里是验证码的四个字母,被one-hot编码后形成的四个 ...
- 基于Kubernetes/K8S构建Jenkins持续集成平台(上)-2
基于Kubernetes/K8S构建Jenkins持续集成平台(上)-2 Kubernetes实现Master-Slave分布式构建方案 传统Jenkins的Master-Slave方案的缺陷 Mas ...
- C#10新特性-全局和隐式usings
.NET 6发布后支持C#10,C# 10 向 C# 语言添加了很多功能,今天我们分享一下全局和隐式usings的使用: using 指令简化了使用命名空间的方式. C# 10 包括一个新的全局 us ...
- 能动的电脑配件「GitHub 热点速览 v.22.11」
看到这个标题就知道硬核的 B 站 UP 主稚晖君又更新了,本次带来的是一个造型可爱的小机器人.除了稚晖君这个一贯硬核的软硬件项目之外,本周也有很多有意思的新项目,像 Linux 服务监控小工具 Ray ...
- tp5 单文件上传接口
路由: Route::post('upload','task/task/upload'); 控制器代码: //文件上传接口 public function upload() { $file = req ...
- PhpStrom 常用的插件
.env files support 可以在env函数使用是提示.env文件中所有的key值的自动完成功能 Markdown support 在编写.md文件时有预览的功能 PHP composer. ...
- Map 集合
<零基础学Java > Map 集合 在程序中需要存储具有映射关系就需要使用Map集合了. Map集合由Map接口.Map接口的实现类组成. Map 接口 Map接口没有Collectio ...
- java的三大特性----封装、集成、多态
当我们被问到你对java的封装.继承.多态是什么看法的时候,你会想到什么? 想到的会不会是封装就是将类的成员属性用privet修饰一下,达到私有化的目的,只暴露方法,从而达到成员变量私有化的目的. 而 ...
- 一文详解Redis键过期策略
摘要:Redis采用的过期策略:惰性删除+定期删除. 本文分享自华为云社区<Redis键过期策略详解>,作者:JavaEdge. 1 设置带过期时间的 key # 时间复杂度:O(1),最 ...