当前位置: 首页 > 科技观察

面试官:你对Es6很了解吗?来实现一个Set

时间:2023-03-21 23:59:08 科技观察

本文转载自微信公众号“前端黑头鱼”作者“黑头鱼”。转载本文请联系前端胖头鱼公众号。PrefaceES6添加了一个新的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,写在这里纯粹是为了方便后续模拟操作。如果你已经很熟悉了,可以直接跳过。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,否则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结果为false,但只有一个NaNs.has(1)//trues.has(NaN)//trues.size//3s.delete(1)s.has(1)//会存入Setfalse。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:"乾隆五"}佛r(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.在学习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)=>{letresulterable=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)){//Iterable对象只进行下一步forOf元素的添加assert(isIterable(iterable),`${iterable}isnotiterable`)//循环iterableobjects,InitializeforOf(iterable,(value)=>{this.add(value)})}}//获取s.size时,会调用size函数返回value数组的长度//https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/getgetsize(){returnthis.value.length}//使用数组的includes方法判断是否包含value//https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/includes//[NaN].includes(NaN)会返回true,而恰好Set只能保存一个NaNhas(value){returnthis.value.includes(value)}//使用has判断值是否存在的方法,如果不存在则添加到数组中,最后返回Set本身,支持链式调用add(value){if(!this.has(value)){this.value。推(value)}returnthis}//删除前先判断该值是否存在,并作为返回值,如果存在则通过拼接方式移除delete(value){letresult=this.has(value)if(result){这个.值。splice(this.value.indexOf(value),1)}returnresult}//重新赋值一个空数组,即实现clear方法clear(){this.value=[]}//遍历values返回的迭代对象通过forOf,实现forEachforEach(callback,thisArg){forOf(this.values(),(value)=>{callback.call(thisArg,value,value,this)})}//返回一个可迭代对象,对象中的value是Set中的valuekeys(){returnnewIterator(this.value)}//同keyvalues(){returnthis.keys()}//返回一个可迭代对象,keys和values的区别在于其valueis[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)//设置{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',']}globals2.forEach(function(){console.log(this)},{name:'qianlongo'})//{name:'qianlongo'}//{name:'qianlongo'}//{name:'qianlongo'}//{name:"qianlongo"}//{name:"qianlongo"}//{name:"qianlongo"}for(letvalueofs){console.log(值)}//s//e//tfor(letvalueofs.entries()){console.log(值)}//["s","s"]//["e","e"]//["t","t"]遍历方式貌似能达到和前面例子一样的效果,源码实现部分基本到达到这里,还没完呢……为什么[...s2]可以得到数组['s','e',']?为什么s2可以被forof循环?iterator(迭代器)从MDN我找到了这段话。在JavaScript中,迭代器是一个对象,它提供next()方法来返回序列中的下一个项目。该方法返回两个属性:done(表示遍历是否结束)和value(当前值)。一旦创建了迭代器对象,就可以重复调用next()。functionmakeIterator(array){varnextIndex=0return{next:function(){returnextIndexvalue){this.value=Array.from(arrayLike)this.nextIndex=0this.len=this.value.lengththis.iteratee=iteratee}next(){letdone=this.nextIndex>=this.lenletvalue=done?undefined:this.iteratee(this.value[this.nextIndex++])return{done,value}}[Symbol.iterator](){returnthis}}Iterator的实例有一个next方法,每次调用都会返回一个done属性和value属性具有与上面解释的相同的语义。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需要实现的功能之一是提供一个迭代器。那么这与上面的问题1和2有什么关系呢?下面我们来看一个数据结构的forofforof。只要部署了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”接口,而spreadoperator的特点之一……就是任何对象只要有Iterator接口可以用扩展运算符转换成一个真正的数组。最终仿真过程中可能会出现相应的错误,与原来的实现不完全一样。仅供学习,欢迎大家拍砖。参考SimulationofSetIteratorandGeneratorES6系列实现一个Set数据结构Expandsyntaxfor...ofloop