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);}
验证是否是身份证