在面试中,常常会遇到一些手写XXX之类的面试题,因此好好总结一下,对于巩固我们的原生js的基础是非常必要的。
尽管在网上已经有了非常多的总结文章,但在我看来有一个普遍的问题,那就是把原理性的东西过于复杂化了。如果站在面试官的角度,他的目的是在最短的时间内考察出面试者对于JS语言的理解程度,但是在看了网站的诸多总结文章后我发现其中的代码有很大一部分是做意义不大的操作,比如实现一个简单的防抖,只要是核心的流程展示即可,至于其他的一些等模式则没有必要再去深挖,一大堆的if-else让人看上去也眼花缭乱,甚至误导别人直接去背代码,另外,核心的逻辑都能展示出来,再去横向的实现其他的类似情况恐怕也不是什么问题了。
在以下的整理中,建议大家先照的核心要点自己写一遍,然后对照下面的代码,复习效果更好。本文的目的就在于以最简洁的代码帮你从第一性原理的角度理解api的内部运作流程,凡是对于我们理解api没有帮助的的边界情况都不做处理。
核心要点:
1.回调函数的参数有哪些,返回值如何处理。
2.不修改原来的数组。
Array.prototype.MyMap = function(fn, context){ var arr = Array.prototype.slice.call(this);//由于是ES5所以就不用...展开符了 var mAppedArr = []; for (var i = 0; i < arr.length; i++ ){ mappedArr.push(fn.call(context, arr[i], i, this)); } return mappedArr; }
核心要点:
1、初始值不传怎么处理
2、回调函数的参数有哪些,返回值如何处理。
Array.prototype.myReduce = function(fn, initialValue) { var arr = Array.prototype.slice.call(this); var res, startIndex; res = initialValue ? initialValue : arr[0]; startIndex = initialValue ? 0 : 1; for(var i = startIndex; i < arr.length; i++) { res = fn.call(null, res, arr[i], i, this); } return res; }
思路: 利用this的上下文特性。
//实现apply只要把下一行中的...args换成args即可 Function.prototype.myCall = function(context = window, ...args) { let func = this; let fn = Symbol("fn"); context[fn] = func; let res = context[fn](...args "fn");//重点代码,利用this指向,相当于context.caller(...args) delete context[fn]; return res; }
function create(proto) { function F() {}; F.prototype = proto; F.prototype.constructor = F; return new F(); }
核心要点:
1.对于普通函数,绑定this指向
2.对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.bind = function(context, ...args) { let self = this;//谨记this表示调用bind的函数 let fBound = function() { //this instanceof fBound为true表示构造函数的情况。如new func.bind(obj) return self.apply(this instanceof fBound ? this : context || window, args); } fBound.prototype = Object.create(this.prototype);//保证原函数的原型对象上的属性不丢失 return fBound; }
大家平时说的手写bind,其实就这么简单:)
核心要点:
function myNew(fn, ...args) { let instance = Object.create(fn.prototype); let res = fn.apply(instance, args); return typeof res === 'object' ? res: instance; }
核心要点:原型链的向上查找。
function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left); while(true) { if(proto == null) return false; if(proto == right.prototype) return true; proto = Object.getPrototypeof(proto); } }
核心要点: 用闭包和Proxy属性拦截
function proxy(func) { let instance; let handler = { constructor(target, args) { if(!instance) { instance = Reflect.constructor(fun, args); } return instance; } } return new Proxy(func, handler); }
方式其实很多,之前我做过系统整理,有六种方法,请参考:
JS数组扁平化(flat)方法总结[1]
核心要点:
如果在定时器的时间范围内再次触发,则重新计时。
const debounce = (fn, delay) => { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; };
核心要点:
如果在定时器的时间范围内再次触发,则不予理睬,等当前定时器完成,才能启动下一个定时器。
const throttle = (fn, delay = 500) => { let flag = true; return (...args) => { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, args); flag = true; }, delay); }; };
以下为简易版深拷贝,没有考虑循环引用的情况和Buffer、Promise、Set、Map的处理,如果一一实现,过于复杂,面试短时间写出来不太现实,如果有兴趣可以去这里深入实现:
深拷贝终极探索[2]。
const clone = parent => { // 判断类型 const isType = (target, type) => `[object ${type}]` === Object.prototype.toString.call(target) // 处理正则 const getRegExp = re => { let flags = ""; if (re.global) flags += "g"; if (re.ignoreCase) flags += "i"; if (re.multiline) flags += "m"; return flags; }; const _clone = parent => { if (parent === null) return null; if (typeof parent !== "object") return parent; let child, proto; if (isType(parent, "Array")) { // 对数组做特殊处理 child = []; } else if (isType(parent, "RegExp")) { // 对正则对象做特殊处理 child = new RegExp(parent.source, getRegExp(parent)); if (parent.lastIndex) child.lastIndex = parent.lastIndex; } else if (isType(parent, "Date")) { // 对Date对象做特殊处理 child = new Date(parent.getTime()); } else { // 处理对象原型 proto = Object.getPrototypeOf(parent); // 利用Object.create切断原型链 child = Object.create(proto); } for (let i in parent) { // 递归 child[i] = _clone(parent[i]); } return child; }; return _clone(parent); };