当前位置: 首页 > Web前端 > HTML5

从零到模拟实现一个Set类

时间:2023-04-05 00:57:23 HTML5

前言ES6增加了一个新的Set数据结构,它允许你存储任何类型的唯一值,无论是原始值还是对象引用。本文希望通过模拟Set来增加对Set的理解。原文链接是之前实际工作和学习过程中使用的。您可能还经常使用Set来删除重复数组letunique=(array)=>{return[...newSet(array)]}console.log(unique([1,2,3,4,1,2,5]))//[1,2,3,4,5]基本语法以下内容基本来自MDN,写在这里纯粹是为了后面Simulation操作方便。如果你已经很熟悉了,可以直接跳过。newSet([iterable])可以传递一个可迭代对象,其元素将被添加到新的Set中。如果未指定此参数或其值为空,则新的Set为空。lets=newSet([1,2,3])//Set(3){1,2,3}lets2=newSet()//Set(0){}lets3=newSet(null/*orundefined*/)//Set(0){}实例属性和方法属性构造函数Set的构造函数大小Set长度操作方法Set.prototype.add(value)在Set对象的末尾添加一个元素。返回集合对象。Set.prototype.has(value)返回一个布尔值,指示该值是否存在于Set中。set.prototype.delete(value)在Set中移除等于这个值的元素,并返回Set.prototype.has(value)在本次操作之前会返回的值(即如果元素存在则返回true,否则返回true)returnfalse)Set.prototype.clear()删除Set对象中的所有元素。无返回值栗子lets=newSet()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结果为假,但只有一个NaNs.has(1)//trues.has(NaN)//trues.size//3s.delete(1)s.has(1)会存入Set//falses.size//2s.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将是此参数。栗子lets=newSet(['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)})//ssSet(3){"s","e","t"}Window//eeSet(3){"s","e","t"}Window//ttSet(3){"s","e","t"}Windows.forEach(function(){console.log(this)},{name:'qianlongo'})//{name:"qianlongo"}//{name:"qianlongo"}//{name:"qianlongo"}for(letvalueofs){console.log(value)}//s//e//tfor(letvalueofs.entries()){console.log(value))}//["s","s"]//["e","e"]//["t","t"]整体结构回顾了上面Set的基本使用,我们可以开始尝试模拟实现一。也可以直接点击查看源码。目录结构├──set-polyfill│├──iterator.js//导出一个构造函数Iterator,模拟创建可迭代对象│├──set.js//设置类│├──utils.js//辅助函数│├──test.js//测试Set整体框架classSet{constructor(iterable){}getsize(){}has(){}add(){}delete(){}clear(){}forEach(){}keys(){}values(){}entries(){}[Symbol.iterator](){}}辅助方法在开始实现Set细节之前,先来看看一些辅助方法会用到assert,这个方法在学习vuex源码的时候看到过。感觉挺实用的。主要用来判断某些条件,抛出错误。constassert=(condition,msg)=>{if(!condition)thrownewError(msg)}isDef,过滤掉null和undefinedconstisDef=(value)=>{returnvalue!=void0}isIterable,简单判断value是否为迭代器对象。constisIterable=(value)=>{returnisDef(value)&&typeofvalue[Symbol.iterator]==='function'}forOf,模拟forof的行为,遍历iterator对象。constforOf=(iterable,callback,ctx)=>{letresultiterable=iterable[Symbol.iterator]()result=iterable.next()while(!result.done){callback.call(ctx,result.value)result=iterable.next()}}源码实现classSet{constructor(iterable){//使用数组存储Set的每个元素this.value=[]//判断是否使用new调用assert(thisinstanceofSet,'ConstructorSetrequires"new"')//过滤掉null和undefinedif(isDef(iterable)){//只有可迭代对象才会进行下一步forOf元素添加assert(isIterable(iterable),`${iterable}isnotiterable`)//循环迭代对象,初始化forOf(iterable,(value)=>{this.add(value)})}}//获取s.size时,会调用size函数返回值数组的长度//https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/getgetsize(){returnthis.value.length}//使用includes数组判断值是否包含的方法//https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/includes//[NaN].includes(NaN)将返回true,positive好的Set只能保存一个NaNhas(value){returnthis.value.includes(value)}//使用has方法判断value是否存在,如果不存在则添加到数组中,最后returnSet本身,支持链式调用add(value){if(!this.has(value)){this.value.push(value)}returnthis}//删除前判断value是否存在并使用作为返回值,如果存在则通过splice方法移除delete(value){letresult=this.has(value)if(result){this.value.splice(this.value.indexOf(value),1)}returnresult}//重新赋值一个空数组,即实现clear方法clear(){this.value=[]}//通过forOf遍历values返回的可迭代对象,实现forEachforEach(callback,thisArg){forOf(this.values(),(value)=>{callback.call(thisArg,value,value,this)})}//返回一个可迭代对象,对象中的值为对象中的值setkeys(){returnnewIterator(this.value)}//同keysvalues(){returnthis.keys()}//返回一个迭代器对象,keys和values的区别是[value,value]entries(){returnnewIterator(this.value,(value)=>[value,value])}//返回一个新的迭代器对象,该对象按插入顺序包含Set对象中所有元素的值[Symbol.iterator](){returnthis.values()}}测试一个节点test.jssize属性和操作方法constSet=require('./set')consts=newSet()s.add(1).add(2).add(NaN).add(NaN)console.log(s)//Set{value:[1,2,NaN]}console.log(s.has(1))//trueconsole.log(s.has(NaN))//trueconsole.log(s.size)//3s.delete(1)console.log(s.has(1))//falseconsole.log(s.size)//2s.clear()console.log(s)//Set{value:[]}上面的例子已经遍历了Set的size属性和操作方法,打印出的Set实例看起来和原来的不一样,忽略即可.遍历方法lets2=newSet(['s','e','t'])console.log(s2)//Set{value:['s','e','t']}console.log(s2.keys())//迭代器{}console.log(s2.values())//迭代器{}console.log(s2.entries())//迭代器{}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)})//ssSet{value:['s','e','t']}global//eeSet{value:['s','e','t']}global//ttSet{value:['s','e','t']}globals2.forEach(function(){console.log(this)},{name:'qianlongo'})//{name:'qianlongo'}//{name:'qianlongo'}//{name:'qianlongo'}//{name:"qianlongo"}//{name:"qianlongo"}//{name:"qianlongo"}for(lets的值){console.log(value)}//s//e//tfor(letvalueofs.entries()){console.log(value)}//["s","s"]//["e","e"]//["t","t"]遍历方式貌似可以达到和前面一样的效果例子,源码的实现部分基本就到这里了,但是还没完……为什么[...s2]可以得到数组['s','e','t']?为什么s2可以被forof循环?Iterator(迭代器)从MDN上找到了这段话。在JavaScript中,迭代器是一个对象,它提供next()方法来返回序列中的下一个项目。该方法返回两个属性:done(表示遍历是否结束)和value(当前值)。一旦创建了迭代器对象,就可以重复调用next()。functionmakeIterator(array){varnextIndex=0return{next:function(){returnnextIndexvalue){this.value=Array.from(arrayLike)this.nextIndex=0this.len=this.value.lengththis.iteratee=iteratee}next(){让done=this.nextIndex>=this.len让value=done?undefined:this.iteratee(this.value[this.nextIndex++])return{done,value}}[Symbol.iterator](){returnthis}}Iterator实例有一个next方法,每次调用都会返回一个done属性和一个值属性,其语义与前面的解释相同。letit=newIterator(['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需要实现的功能之一就是Provide一个迭代器。那么这与上面的问题1和2有什么关系呢?我们再来看看foroffor。只要一个数据结构部署了Symbol.iterator属性,就认为它有一个迭代器接口,它的成员可以用for...of遍历。也就是说for...of循环内部调用了数据结构的Symbol.iterator方法。for...of循环默认只有(Array,Map,Set,String,TypedArray,arguments)可以被forof迭代。我们自定义的Set类不在其中,但是在前面的例子中,在forof循环中打印了想要的值。原因是我们在Iterator类中部署了Symbol.iterator方法,执行这个方法会返回Iterator实例本身,它是一个可以迭代的对象。[Symbol.iterator](){returnthis}上面的问题2可以在这里解释。再看问题1。为什么[...s2]可以得到数组['s','e','t']?,原因是我们为Set、keys、values和entry部署了Symbol.iterator,使它们具有“迭代器”接口,而扩展运算符的特点之一……就是任何具有Iterator的对象接口可用于扩展操作字符转化为真正的数组。最终仿真过程中可能会出现相应的错误,与原来的实现不完全一样。仅供学习,欢迎大家拍砖。原文链接参考了ES6系列的Set迭代器和生成器的模拟实现一个Set数据结构扩展语法for...of循环