分享:22道JavaScript高频手写面试题

JavaScript笔试部分

实现防抖函数(debounce)

防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

那么与节流函数的区别直接看这个动画实现即可。

手写简化版:

// 防抖函数constdebounce =(fn, delay) =>{lettimer =null;return(...args) =>{    clearTimeout(timer);    timer = setTimeout(()=>{      fn.apply(this, args);    }, delay);  };};

适用场景:

按钮提交场景:防止多次提交按钮,只执行最后提交的一次

服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

生存环境请用lodash.debounce

实现节流函数(throttle)

防抖函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

// 手写简化版

// 节流函数constthrottle =(fn, delay =500) =>{letflag =true;return(...args) =>{if(!flag)return;    flag =false;    setTimeout(()=>{      fn.apply(this, args);      flag =true;    }, delay);  };};

适用场景:

拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动

缩放场景:监控浏览器resize

动画场景:避免短时间内多次触发动画引起性能问题

深克隆(deepclone)

简单版:

constnewObj =JSON.parse(JSON.stringify(oldObj));

局限性:

他无法实现对函数 、RegExp等特殊对象的克隆

会抛弃对象的constructor,所有的构造函数会指向Object

对象有循环引用,会报错

面试版:

/** * deep clone *@param{[type]} parent object 需要进行克隆的对象 *@return{[type]}        深克隆后的对象 */constclone=parent=> {// 判断类型constisType = (obj, type) => {if(typeof obj !=="object")returnfalse;consttypeString = Object.prototype.toString.call(obj);    let flag;switch(type) {case"Array":        flag = typeString ==="[object Array]";break;case"Date":        flag = typeString ==="[object Date]";break;case"RegExp":        flag = typeString ==="[object RegExp]";break;default:        flag =false;    }returnflag;  };// 处理正则constgetRegExp = re => {varflags ="";if(re.global) flags +="g";if(re.ignoreCase) flags +="i";if(re.multiline) flags +="m";returnflags;  };// 维护两个储存循环引用的数组constparents = [];constchildren = [];const_clone =parent=> {if(parent===null)returnnull;if(typeofparent!=="object")returnparent;    let child, proto;if(isType(parent,"Array")) {// 对数组做特殊处理child = [];    }elseif(isType(parent,"RegExp")) {// 对正则对象做特殊处理child =newRegExp(parent.source, getRegExp(parent));if(parent.lastIndex) child.lastIndex =parent.lastIndex;    }elseif(isType(parent,"Date")) {// 对Date对象做特殊处理child =newDate(parent.getTime());    }else{// 处理对象原型proto = Object.getPrototypeOf(parent);// 利用Object.create切断原型链child = Object.create(proto);    }// 处理循环引用constindex = parents.indexOf(parent);if(index !=-1) {// 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象returnchildren[index];    }    parents.push(parent);    children.push(child);for(let i inparent) {// 递归child[i] = _clone(parent[i]);    }returnchild;  };return_clone(parent);};

局限性:

一些特殊情况没有处理: 例如Buffer对象、Promise、Set、Map

另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间

实现Event(event bus)

event bus既是node中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。

简单版:

classEventEmeitter{constructor() {this._events =this._events || new Map();// 储存事件/回调键值对this._maxListeners =this._maxListeners ||10;// 设立监听上限}}// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) {  let handler;// 从储存事件键值对的this._events中获取对应事件回调函数handler =this._events.get(type);if(args.length >0) {    handler.apply(this, args);  }else{    handler.call(this);  }returntrue;};// 监听名为type的事件EventEmeitter.prototype.addListener = function(type, fn) {// 将type事件以及对应的fn函数放入this._events中储存if(!this._events.get(type)) {this._events.set(type, fn);  }};

面试版:

classEventEmeitter {constructor() {this._events =this._events ||newMap();// 储存事件/回调键值对this._maxListeners =this._maxListeners ||10;// 设立监听上限}}// 触发名为type的事件EventEmeitter.prototype.emit =function(type, ...args){lethandler;// 从储存事件键值对的this._events中获取对应事件回调函数handler =this._events.get(type);if(args.length >0) {    handler.apply(this, args);  }else{    handler.call(this);  }returntrue;};// 监听名为type的事件EventEmeitter.prototype.addListener =function(type, fn){// 将type事件以及对应的fn函数放入this._events中储存if(!this._events.get(type)) {this._events.set(type, fn);  }};// 触发名为type的事件EventEmeitter.prototype.emit =function(type, ...args){lethandler;  handler =this._events.get(type);if(Array.isArray(handler)) {// 如果是一个数组说明有多个监听者,需要依次此触发里面的函数for(leti =0; i < handler.length; i++) {if(args.length >0) {        handler[i].apply(this, args);      }else{        handler[i].call(this);      }    }  }else{// 单个函数的情况我们直接触发即可if(args.length >0) {      handler.apply(this, args);    }else{      handler.call(this);    }  }returntrue;};// 监听名为type的事件EventEmeitter.prototype.addListener =function(type, fn){consthandler =this._events.get(type);// 获取对应事件名称的函数清单if(!handler) {this._events.set(type, fn);  }elseif(handler &&typeofhandler ==="function") {// 如果handler是函数说明只有一个监听者this._events.set(type, [handler, fn]);// 多个监听者我们需要用数组储存}else{    handler.push(fn);// 已经有多个监听者,那么直接往数组里push函数即可}};EventEmeitter.prototype.removeListener =function(type, fn){consthandler =this._events.get(type);// 获取对应事件名称的函数清单// 如果是函数,说明只被监听了一次if(handler &&typeofhandler ==="function") {this._events.delete(type, fn);  }else{letpostion;// 如果handler是数组,说明被监听多次要找到对应的函数for(leti =0; i < handler.length; i++) {if(handler[i] === fn) {        postion = i;      }else{        postion =-1;      }    }// 如果找到匹配的函数,从数组中清除if(postion !==-1) {// 找到数组对应的位置,直接清除此回调handler.splice(postion,1);// 如果清除后只有一个函数,那么取消数组,以函数形式保存if(handler.length ===1) {this._events.set(type, handler[0]);      }    }else{returnthis;    }  }};

实现instanceOf

// 模拟 instanceoffunctioninstance_of(L, R){//L 表示左表达式,R 表示右表达式varO = R.prototype;// 取 R 的显示原型L = L.__proto__;// 取 L 的隐式原型while(true) {if(L ===null)returnfalse;if(O === L)// 这里重点:当 O 严格等于 L 时,返回 truereturntrue;    L = L.__proto__;  }}

模拟new

new操作符做了这些事:

它创建了一个全新的对象

它会被执行[[Prototype]](也就是__proto__)链接

它使this指向新创建的对象

通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上

如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用

// objectFactory(name, 'cxk', '18')functionobjectFactory(){constobj =newObject();constConstructor = [].shift.call(arguments);  obj.__proto__ = Constructor.prototype;constret = Constructor.apply(obj,arguments);returntypeofret ==="object"? ret : obj;}

实现一个call

call做了什么:

将函数设为对象的属性

执行&删除这个函数

指定this到函数并传入给定参数执行函数

如果不传入参数,默认指向为 window

// 模拟 call bar.mycall(null);//实现一个call方法:Function.prototype.myCall =function(context){//此处没有考虑context非object情况context.fn =this;letargs = [];for(leti =1, len =arguments.length; i < len; i++) {    args.push(arguments[i]);  }  context.fn(...args);letresult = context.fn(...args);deletecontext.fn;returnresult;};

具体实现参考 JavaScript深入之call和apply的模拟实现

实现apply方法

apply原理与call很相似,不多赘述

// 模拟 applyFunction.prototype.myapply =function(context, arr){varcontext =Object(context) ||window;  context.fn =this;varresult;if(!arr) {    result = context.fn();  }else{varargs = [];for(vari =0, len = arr.length; i < len; i++) {      args.push("arr["+ i +"]");    }    result =eval("context.fn("+ args +")");  }deletecontext.fn;returnresult;};

实现bind

实现bind要做什么

返回一个函数,绑定this,传递预置参数

bind返回的函数可以作为构造函数使用。故作为构造函数时应使得this失效,但是传入的参数依然有效

// mdn的实现if(!Function.prototype.bind) {Function.prototype.bind =function(oThis){if(typeofthis!=='function') {// closest thing possible to the ECMAScript 5// internal IsCallable functionthrownewTypeError('Function.prototype.bind - what is trying to be bound is not callable');    }varaArgs  =Array.prototype.slice.call(arguments,1),        fToBind =this,        fNOP    =function(){},        fBound  =function(){// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用returnfToBind.apply(thisinstanceoffBound                ?this: oThis,// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的aArgs.concat(Array.prototype.slice.call(arguments)));        };// 维护原型关系if(this.prototype) {// Function.prototype doesn't have a prototype propertyfNOP.prototype =this.prototype;    }// 下行的代码使fBound.prototype是fNOP的实例,因此// 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例fBound.prototype =newfNOP();returnfBound;  };}

详解请移步 JavaScript深入之bind的模拟实现 #12

模拟Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

// 模拟 Object.createfunctioncreate(proto){functionF(){}  F.prototype = proto;returnnewF();}

实现类的继承

类的继承在几年前是重点内容,有n种继承方式各有优劣,es6普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。

functionParent(name){this.parent = name}Parent.prototype.say =function(){console.log(`${this.parent}: 你打篮球的样子像kunkun`)}functionChild(name, parent){// 将父类的构造函数绑定在子类上Parent.call(this, parent)this.child = name}/**

1. 这一步不用Child.prototype =Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类

2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性

3. Object.create是创建了父类原型的副本,与父类原型完全隔离

*/Child.prototype =Object.create(Parent.prototype);Child.prototype.say =function(){console.log(`${this.parent}好,我是练习时长两年半的${this.child}`);}// 注意记得把子类的构造指向子类本身Child.prototype.constructor = Child;varparent =newParent('father');parent.say()// father: 你打篮球的样子像kunkunvarchild =newChild('cxk','father');child.say()// father好,我是练习时长两年半的cxk

实现JSON.parse

varjson = '{"name":"cxk","age":25}';varobj = eval("("+ json +")");

此方法属于黑魔法,极易容易被xss攻击,还有一种 new Function 大同小异。

简单的教程看这个 半小时实现一个 JSON 解析器

实现Promise

我很早之前实现过一版,而且注释很多,但是居然找不到了,这是在网络上找了一版带注释的,目测没有大问题,具体过程可以看这篇 史上最易读懂的 Promise/A+ 完全实现

varPromisePolyfill = (function(){// 和reject不同的是resolve需要尝试展开thenable对象functiontryToResolve(value){if(this=== value) {// 主要是防止下面这种情况// let y = new Promise(res => setTimeout(res(y)))throwTypeError('Chaining cycle detected for promise!')    }// 根据规范2.32以及2.33 对对象或者函数尝试展开// 保证S6之前的 polyfill 也能和ES6的原生promise混用if(value !==null&&      (typeofvalue ==='object'||typeofvalue ==='function')) {try{// 这里记录这次then的值同时要被try包裹// 主要原因是 then 可能是一个getter, 也也就是说//  1. value.then可能报错//  2. value.then可能产生副作用(例如多次执行可能结果不同)varthen = value.then// 另一方面, 由于无法保证 then 确实会像预期的那样只调用一个onFullfilled / onRejected// 所以增加了一个flag来防止resolveOrReject被多次调用varthenAlreadyCalledOrThrow =falseif(typeofthen ==='function') {// 是thenable 那么尝试展开// 并且在该thenable状态改变之前this对象的状态不变then.bind(value)(// onFullfilledfunction(value2){if(thenAlreadyCalledOrThrow)returnthenAlreadyCalledOrThrow =truetryToResolve.bind(this, value2)()            }.bind(this),// onRejectedfunction(reason2){if(thenAlreadyCalledOrThrow)returnthenAlreadyCalledOrThrow =trueresolveOrReject.bind(this,'rejected', reason2)()            }.bind(this)          )        }else{// 拥有then 但是then不是一个函数 所以也不是thenableresolveOrReject.bind(this,'resolved', value)()        }      }catch(e) {if(thenAlreadyCalledOrThrow)returnthenAlreadyCalledOrThrow =trueresolveOrReject.bind(this,'rejected', e)()      }    }else{// 基本类型 直接返回resolveOrReject.bind(this,'resolved', value)()    }  }functionresolveOrReject(status, data){if(this.status !=='pending')returnthis.status = statusthis.data = dataif(status ==='resolved') {for(vari =0; i {constresult = []letcnt =0for(leti =0; i < promises.length; ++i) {      promises[i].then(value=>{        cnt++        result[i] = valueif(cnt === promises.length) resolve(result)      }, reject)    }  })}PromisePolyfill.race =function(promises){returnnewPromise((resolve, reject) =>{for(leti =0; i < promises.length; ++i) {      promises[i].then(resolve, reject)    }  })}

解析 URL Params 为对象

let url ='http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';parseParam(url)/* 结果{user:'anonymous',id:[123,456],//重复出现的 key 要组装成数组,能被转成数字的就转成数字类型city:'北京',//中文需解码enabled:true,//未指定值得 key 约定为true}*/

functionparseParam(url){constparamsStr =/.+\?(.+)$/.exec(url)[1];// 将 ? 后面的字符串取出来constparamsArr = paramsStr.split('&');// 将字符串以 & 分割后存到数组中letparamsObj = {};// 将 params 存到对象中paramsArr.forEach(param=>{if(/=/.test(param)) {// 处理有 value 的参数let[key, val] = param.split('=');// 分割 key 和 valueval =decodeURIComponent(val);// 解码val =/^\d+$/.test(val) ?parseFloat(val) : val;// 判断是否转为数字if(paramsObj.hasOwnProperty(key)) {// 如果对象有 key,则添加一个值paramsObj[key] = [].concat(paramsObj[key], val);      }else{// 如果对象没有这个 key,创建 key 并设置值paramsObj[key] = val;      }    }else{// 处理没有 value 的参数paramsObj[param] =true;    }  })returnparamsObj;}

模板引擎实现

lettemplate ='我是{{name}},年龄{{age}},性别{{sex}}';letdata = {  name:'姓名',  age:18}render(template, data); // 我是姓名,年龄18,性别undefined

functionrender(template, data){constreg = /\{\{(\w+)\}\}/;// 模板字符串正则if(reg.test(template)) {// 判断模板里是否有模板字符串constname = reg.exec(template)[1];// 查找当前模板里第一个模板字符串的字段template=template.replace(reg, data[name]);// 将第一个模板字符串渲染returnrender(template, data);// 递归的渲染并返回渲染后的结构}returntemplate;// 如果模板没有模板字符串直接返回}

转化为驼峰命名

vars1 ="get-element-by-id"// 转化为 getElementById

varf =function(s){returns.replace(/-\w/g,function(x){returnx.slice(1).toUpperCase();    })}

查找字符串中出现最多的字符和个数

例: abbcccddddd -> 字符最多的是d,出现了5次

letstr ="abcabcabcbbccccc";letnum =0;letchar =''; // 使其按照一定的次序排列str = str.split('').sort().join('');//"aaabbbbbcccccccc"// 定义正则表达式letre = /(\w)\1+/g;str.replace(re,($0,$1) => {    if(num < $0.length){        num = $0.length;        char = $1;            }});console.log(`字符最多的是${char},出现了${num}次`);

字符串查找

请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。

a='34';b='1234567';//返回2a='35';b='1234567';//返回 -1a='355';b='12354355';//返回5isContain(a,b);

functionisContain(a, b){for(letiinb) {if(a[0] === b[i]) {lettmp =true;for(letjina) {if(a[j] !== b[~~i + ~~j]) {          tmp =false;        }      }if(tmp) {returni;      }    }  }return-1;}

实现千位分隔符

// 保留三位小数parseToMoney(1234.56);// return '1,234.56'parseToMoney(123456789);// return '123,456,789'parseToMoney(1087654.321);// return '1,087,654.321'

functionparseToMoney(num){  num =parseFloat(num.toFixed(3));let[integer, decimal] =String.prototype.split.call(num,'.');  integer = integer.replace(/\d(?=(\d{3})+$)/g,'$&,');returninteger +'.'+ (decimal ? decimal :'');}

正则表达式(运用了正则的前向声明和反前向声明):

functionparseToMoney(str){// 仅仅对位置进行匹配letre =/(?=(?!\b)(\d{3})+$)/g;returnstr.replace(re,','); }

判断是否是电话号码

functionisPhone(tel){varregx =/^1[34578]\d{9}$/;returnregx.test(tel);}

验证是否是邮箱

functionisEmail(email){varregx =/^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;returnregx.test(email);}

验证是否是身份证

functionisCardNo(number){varregx =/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;returnregx.test(number);}

你可能感兴趣的:(分享:22道JavaScript高频手写面试题)