前言最近很多人和我一样在积极准备前端笔试,所以我也整理了一些很容易上手的原题前端笔试面试会被问到各种前端原理的功能实现和实现。能够亲手实现各种JavaScript原生功能,可以说是摆脱API调用者帽子的第一步。我们不仅要知道如何使用它,还要探究它的实现原理!学习和实现JavaScript源代码可以帮助我们快速、稳定地提高你的前端编程技能。要实现一个new运算符,我们首先要知道new是做什么的:创建一个空的简单JavaScript对象(即{});将对象(即设置对象的构造函数)链接到另一个对象;step(1)新建this的对象作为this的上下文;如果函数不返回对象,则返回this。知道了new是做什么的,我们来实现一下functioncreate(Con,...args){//创建一个空对象this.obj={};//将空对象指向构造函数的原型链Object。setPrototypeOf(this.obj,Con.prototype);//obj绑定到构造函数,那么就可以在构造函数中访问属性,即this.obj.Con(args)letresult=Con.apply(this.obj,args);//如果返回结果是一个对象,则return//new方法无效,否则返回objreturnresultinstanceofObject?result:this.obj;}实现一个Array.isArrayArray.myIsArray=function(o){returnObject.prototype.toString.call(Object(o))==='[objectArray]';};实现一个Object.create()方法functioncreate=function(o){varF=function(){};F.prototype=o;returnnewF();};实现一个EventEmitter的真实经历最近在字节跳动面试中被面试官问到,要我手写一个简单的Event类。classEvent{constructor(){//存储事件数据结构//为了快速查找,使用对象(字典)this._cache={}}//绑定on(type,callback){//为了方便和类查找节省空间//将相同类型的事件放到一个数组中//这里的数组是一个队列,遵循先进先出的原则//即新绑定的事件先被触发letfns=(this._cache[type]=this._cache[type]||[])if(fns.indexOf(callback)===-1){fns.push(callback)}returnthis}//解绑off(type,callback){letfns=this._cache[type]if(Array.isArray(fns)){if(callback){letindex=fns.indexOf(callback)if(index!==-1){fns.splice(index,1)}}else{//清除所有fns。length=0}}returnthis}//触发emittrigger(type,data){letfns=this._cache[type]if(Array.isArray(fns)){fns.forEach((fn)=>{fn(data)})}returnthis}//一次性绑定一次(type,callback){letwrapFun=()=>{callback.call(this);this.off(type,callback);};this.on(wrapFun,callback);returnthis;}}lete=newEvent()e.on('click',function(){console.log('on')})e.on('click',function(){console.log('onon')})//e.trigger('click','666')console.log(e)实现一个Array.prototype.reduce首先观察Array.prototype.reduce语法Array.prototype.reduce(callback(accumulator,当前值[,索引[,数组y]])[,initialValue])然后你可以手工实现:Array.prototype.myReduce=function(callback,initialValue){letaccumulator=initialValue?initialValue:this[0];for(leti=initialValue?0:1;i{acc+=val;returnacc;},5);console.log(sum);//15实现一个call或者apply,先看一个call实例,然后看call的最后做了什么:letfoo={value:1};functionbar(){console.log(this.value);}bar.call(foo);//1从代码的执行结果看,我们可以看到调用首先改变了this的指向,使得函数的this指向foo,然后执行bar函数总结一下:call把函数this改成了调用函数想想看:我们如何实现上面的效果呢?代码修改如下:Function.prototype.myCall=function(context){context=context||window;//将函数挂载到对象的fn属性上context.fn=this;//处理传入参数constargs=[...arguments].slice(1);//通过对象的属性调用方法constresult=context.fn(...args);//删除这个属性deletecontext.fn;returnresult};我们看一下上面的代码:首先,我们对参数context做了兼容处理,不传值,context默认值为window;然后我们把函数挂载到context上面,context.fn=this;处理参数,拦截传入myCall的参数,去掉第一个数字,然后转成数组;调用context.fn,此时fn的this指向context;删除对象上的属性删除上下文。fn;返回结果。以此类推,我们顺便实现apply。唯一的区别是参数的处理。代码如下:Function.prototype.myApply=function(context){context=context||windowcontext.fn=thisletresult//myApply的参数形式为(obj,[arg1,arg2,arg3]);//所以myApply的第二个参数是[arg1,arg2,arg3]//这里我们使用展开运算符来处理参数的传入方法if(arguments[1]){result=context.fn(...arguments[1])}else{result=context.fn()}deletecontext.fn;returnresult};以上就是call和apply的模拟实现,区别只是参数的处理方式。实现一个Function.prototype.bindfunctionPerson(){this.name="zs";this.age=18;this.gender="male"}letobj={hobby:"Reading"}//绑定this的构造函数ForobjletchangePerson=Person.bind(obj);//直接调用构造函数,函数会操作obj对象,并为其添加三个属性;changePerson();//1,输出objconsole.log(obj);//使用changethis指向的构造函数,出来一个新的实例letp=newchangePerson();//2,输出objconsole.log(p);仔细观察上面的代码,再看输出结果。我们对Person类使用bind将this指向obj,并获得changeperson函数。这里,如果我们直接调用changeperson,obj就会被改变。如果我们用new调用changeperson,我们会得到一个实例p,它的__proto__指向Person。我们发现绑定失败了。我们得出结论:使用bind改变this指向的函数,如果使用new运算符调用,bind会失效。This对象是this构造函数的一个实例,所以只要在函数内部执行thisinstanceof构造函数判断结果是否为真,就可以通过new运算符判断函数是否被调用,如果结果为真,使用newoperation代码修改如下://bindimplementsFunction.prototype.mybind=function(){//1,保存函数let_this=this;//2,保存目标对象letcontext=arguments[0]||窗口;//3.保存目标对象以外的参数,并转成数组;letrest=Array.prototype.slice.call(arguments,1);//4.返回一个待执行的函数returnfunctionF(){//5,将两次传递的参数转换成一个数组;letrest2=Array.prototype.slice.call(arguments)if(thisinstanceofF){//6。如果使用new运算符调用,则直接用new调用原函数,使用扩展运算符传递参数returnnew_this(...rest2)}else{//7。使用apply调用第一步保存的函数,并绑定this,传递组合参数数组,即context._this(rest.concat(rest2))_this.apply(context,rest.concat(rest2));}}};实现一个JS函数CurryingCurrying的概念其实并不复杂。用通俗易懂的话说:只传递一部分参数给函数调用它,让它返回一个函数来处理剩下的参数。functionprogressCurrying(fn,args){let_this=thisletlen=fn.length;letargs=args||[];returnfunction(){let_args=Array.prototype.slice.call(arguments);Array.prototype.push.apply(args,_args);//如果参数个数小于初始的fn.length,则递归调用,继续收集参数if(_args.length{func.apply(context,args);//context.func(args)},wait);};}//使用window.onscroll=debounce(function(){console.log('debounce');},1000);throttlingthrottlingfunctiononscroll时,每隔一段时间就会触发,functionthrottle(fn,delay){letprevTime=Date.now();returnfunction(){letcurTime=Date.now();if(curTime-prevTime>delay){fn.apply(this,arguments);prevTime=curTime;}};}//使用varthrotteScroll=throttle(function(){console.log('throtte');},1000);window.onscroll=throtteScroll;手写一个JS深拷贝乞丐版JSON.parse(JSON.stringfy));很简单,但是缺陷也很明显,比如复制其他引用类型,复制函数,循环引用等基本版本functionclone(target){if(typeoftarget==='object'){letcloneTarget={};for(这里写的constkeyintarget){cloneTarget[key]=clone(target[key])}returncloneTarget;}else{returntarget}}已经可以帮你应对一些面试官看你的递归解题能力了。但显然,这个深拷贝功能还是存在一些问题的。一个比较完整的深拷贝函数需要同时考虑对象和数组,还要考虑循环引用:[]:{};if(map.get(target)){returntarget;}map.set(target,cloneTarget);for(constkeyintarget){cloneTarget[key]=clone(target[key],map)}返回cloneTarget;}else{returntarget;}}实现一个instanceOf原理:L的proto是否等于R.prototype,不等于找L.__proto__.__proto__untilproto为null//L代表左表达式,R代表右表达式functioninstance_of(L,R){varO=R.prototype;L=L.__proto__;while(true){if(L===null){returnfalse;}//这里关键:当O严格等于L时,返回trueif(O===L){returntrue;}L=L.__proto__;}}原型链继承函数myExtend(C,P){varF=function(){};F.prototype=P.prototype;C.prototype=newF();C.prototype.constructor=C;C.super=P.prototype;}实现一个async/await的原理就是使用生成器(generator)来划分代码片段。然后我们用一个函数让它自己迭代,每一个yield都用一个promise包裹起来。执行下一步的时机由promise控制实现function_asyncToGenerator(fn){returnfunction(){varself=this,args=arguments;//承诺返回值returnnewPromise(function(resolve,reject){//获取迭代器instancevargen=fn.apply(self,args);//执行下一步function_next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,'next',value);}//抛出异常function_throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,'throw',err);}//第一次触发_next(undefined);});};}实现一个Array.prototype.flat()函数最近word在FestivalBeat前端面试的时候,也被面试官问到,要求是手写实现。Array.prototype.myFlat=function(num=1){if(Array.isArray(this)){letarr=[];if(!Number(num)||Number(num)<0){returnthis;}this.forEach(item=>{if(Array.isArray(item)){letcount=numarr=arr.concat(item.myFlat(--count))}else{arr.push(item)}});returnarr;}else{throwtihs+".flatisnotafunction";}};实现事件代理的issue一般会让你说说事件冒泡和事件捕获机制redyellowblue绿色黑色白色实现双向绑定Vue2.xObject.defineProperty版本//dataconstdata={text:'default'};constinput=document.getElementById('input');constspan=document.getElementById('span');//数据劫持Object.defineProperty(data,'text',{//数据变化-->修改视图set(newVal){input.value=newVal;span.innerHTML=newVal;}});//视图变化-->数据变化input.addEventListener('keyup',function(e){data.text=e.target.value;});Vue3.x的代理版本//数据constdata={text:'default'};constinput=document.getElementById('input');constspan=document.getElementById('span');//数据劫持consthandler={set(target,key,value){target[key]=value;//数据变化—>修改视图input.value=value;span.innerHTML=value;returnvalue;}};constproxy=newProxy(data,handler);//视图变化-->数据变化input.addEventListener('keyup',function(e){proxy.text=e.target.价值;});