This article covers various key handwriting tests that are often tested in front-end interviews.
Recommended:
《2021 PHP interview questions summary (collection)》
《2021 big front-end interview questions summary (collection)》
##It is recommended to give priority to:
1. Handwritten instanceof
instanceof function:Determine whether an instance is an instance of its parent class or ancestor type.
instanceof During the search process, the prototype chain of the variable on the left will be traversed until the prototype of the variable on the right is foundThe search fails and false
let myInstanceof = (target,origin) => { while(target) { if(target.__proto__===origin.prototype) { return true } target = target.__proto__ } return false } let a = [1,2,3] console.log(myInstanceof(a,Array)); // true console.log(myInstanceof(a,Object)); // trueis returned
2. Implement the map method of the array
Themap() method of the array will return a new array, and each element in this new array corresponds to The return value after calling the provided function once on the corresponding element in the original array.
Usage:
const a = [1, 2, 3, 4]; const b = array1.map(x => x * 2); console.log(b); // Array [2, 4, 6, 8]
Native implementation:
Array.prototype.myMap = function(fn, thisValue) { let res = [] thisValue = thisValue||[] let arr = this for(let i in arr) { res.push(fn(arr[i])) } return res }
3. Reduce implements the map method of the array
Use the built-in reduce method of the array to implement the map method and examine the mastery of the reduce principleArray.prototype.myMap = function(fn,thisValue){ var res = []; thisValue = thisValue||[]; this.reduce(function(pre,cur,index,arr){ return res.push(fn.call(thisValue,cur,index,arr)); },[]); return res; } var arr = [2,3,1,5]; arr.myMap(function(item,index,arr){ console.log(item,index,arr); })
4. Reduce method of handwritten array
reduce The () method receives a function as an accumulator. Each value in the array (from left to right) begins to reduce, and finally becomes a value. It is another new array item-by-item processing method in ES5Parameters:
function reduce(arr, cb, initialValue){ var num = initValue == undefined? num = arr[0]: initValue; var i = initValue == undefined? 1: 0 for (i; i<p>5. Array flattening<strong></strong></p>Array flattening is to convert a multi-dimensional array into a one-dimensional array<p></p><p>1. New method provided by es6 flat(depth)<strong></strong></p><pre class="brush:php;toolbar:false">let a = [1,[2,3]]; a.flat(); // [1,2,3] a.flat(1); //[1,2,3]In fact, there is a simpler way, without knowing the dimensions of the array, directly convert the target array into a 1-dimensional array. The value of depth is set to Infinity.
let a = [1,[2,3,[4,[5]]]]; a.flat(Infinity); // [1,2,3,4,5] a是4维数组
2. Using cancat
function flatten(arr) { var res = []; for (let i = 0, length = arr.length; i <p>6. Function currying<strong></strong></p>The definition of currying: receiving a part Parameters, return a function to receive the remaining parameters, and after receiving enough parameters, execute the original function. <p></p>When the curried function receives enough parameters, it will execute the original function. How to determine when enough parameters are reached? <p></p>There are two ideas: <p></p>
/** * 将函数柯里化 * @param fn 待柯里化的原函数 * @param len 所需的参数个数,默认为原函数的形参个数 */ function curry(fn,len = fn.length) { return _curry.call(this,fn,len) } /** * 中转函数 * @param fn 待柯里化的原函数 * @param len 所需的参数个数 * @param args 已接收的参数列表 */ function _curry(fn,len,...args) { return function (...params) { let _args = [...args,...params]; if(_args.length >= len){ return fn.apply(this,_args); }else{ return _curry.call(this,fn,len,..._args) } } }Let’s verify it:
let _fn = curry(function(a,b,c,d,e){ console.log(a,b,c,d,e) }); _fn(1,2,3,4,5); // print: 1,2,3,4,5 _fn(1)(2)(3,4,5); // print: 1,2,3,4,5 _fn(1,2)(3,4)(5); // print: 1,2,3,4,5 _fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5Our commonly used tool library lodash also provides the curry method, and adds a very interesting placeholder function through placeholders. Change the order of parameters passed in. For example, if we pass in a placeholder, the parameters passed in this call will skip the placeholder, and the position of the placeholder will be filled by the parameters of the next call, such as this:
Look directly at the example on the official website: Next let’s think about how to implement the placeholder function. For the curry function of lodash, the curry function is mounted on the lodash object, so the lodash object is used as the default placeholder. The curry function we implemented ourselves is not mounted on any object, so we use the curry function as the default placeholder Use placeholders in order to change the parameter transfer order, so in the curry function implementation, it is necessary to record whether a placeholder is used each time, and record the parameter position represented by the placeholder. Go directly to the code:
/** * @param fn 待柯里化的函数 * @param length 需要的参数个数,默认为函数的形参个数 * @param holder 占位符,默认当前柯里化函数 * @return {Function} 柯里化后的函数 */ function curry(fn,length = fn.length,holder = curry){ return _curry.call(this,fn,length,holder,[],[]) } /** * 中转函数 * @param fn 柯里化的原函数 * @param length 原函数需要的参数个数 * @param holder 接收的占位符 * @param args 已接收的参数列表 * @param holders 已接收的占位符位置列表 * @return {Function} 继续柯里化的函数 或 最终结果 */ function _curry(fn,length,holder,args,holders){ return function(..._args){ //将参数复制一份,避免多次操作同一函数导致参数混乱 let params = args.slice(); //将占位符位置列表复制一份,新增加的占位符增加至此 let _holders = holders.slice(); //循环入参,追加参数 或 替换占位符 _args.forEach((arg,i)=>{ //真实参数 之前存在占位符 将占位符替换为真实参数 if (arg !== holder && holders.length) { let index = holders.shift(); _holders.splice(_holders.indexOf(index),1); params[index] = arg; } //真实参数 之前不存在占位符 将参数追加到参数列表中 else if(arg !== holder && !holders.length){ params.push(arg); } //传入的是占位符,之前不存在占位符 记录占位符的位置 else if(arg === holder && !holders.length){ params.push(arg); _holders.push(params.length - 1); } //传入的是占位符,之前存在占位符 删除原占位符位置 else if(arg === holder && holders.length){ holders.shift(); } }); // params 中前 length 条记录中不包含占位符,执行函数 if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){ return fn.apply(this,params); }else{ return _curry.call(this,fn,length,holder,params,_holders) } } }Verify:;
let fn = function(a, b, c, d, e) { console.log([a, b, c, d, e]); } let _ = {}; // 定义占位符 let _fn = curry(fn,5,_); // 将函数柯里化,指定所需的参数个数,指定所需的占位符 _fn(1, 2, 3, 4, 5); // print: 1,2,3,4,5 _fn(_, 2, 3, 4, 5)(1); // print: 1,2,3,4,5 _fn(1, _, 3, 4, 5)(2); // print: 1,2,3,4,5 _fn(1, _, 3)(_, 4,_)(2)(5); // print: 1,2,3,4,5 _fn(1, _, _, 4)(_, 3)(2)(5); // print: 1,2,3,4,5 _fn(_, 2)(_, _, 4)(1)(3)(5); // print: 1,2,3,4,5At this point, we have completely implemented a curry function~~
7 . Implement deep copy
The difference between shallow copy and deep copy:
Shallow copy: only copies one layer, and only copies references at the deeper object level Deep copy: copy multiple layers,data at each level will be copied. In this way, changing the copy value will not affect other objects
ES6 shallow copy method: Object.assign(target,...sources)let obj={ id:1, name:'Tom', msg:{ age:18 } } let o={} //实现深拷贝 递归 可以用于生命游戏那个题对二维数组的拷贝, //但比较麻烦,因为已知元素都是值,直接复制就行,无需判断 function deepCopy(newObj,oldObj){ for(var k in oldObj){ let item=oldObj[k] //判断是数组?对象?简单类型? if(item instanceof Array){ newObj[k]=[] deepCopy(newObj[k],item) }else if(item instanceof Object){ newObj[k]={} deepCopy(newObj[k],item) }else{ //简单数据类型,直接赋值 newObj[k]=item } } }
8. Handwritten call, apply, bind
手写call
Function.prototype.myCall=function(context=window){ // 函数的方法,所以写在Fuction原型对象上 if(typeof this !=="function"){ // 这里if其实没必要,会自动抛出错误 throw new Error("不是函数") } const obj=context||window //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined obj.fn=this //this为调用的上下文,this此处为函数,将这个函数作为obj的方法 const arg=[...arguments].slice(1) //第一个为obj所以删除,伪数组转为数组 res=obj.fn(...arg) delete obj.fn // 不删除会导致context属性越来越多 return res }
//用法:f.call(obj,arg1) function f(a,b){ console.log(a+b) console.log(this.name) } let obj={ name:1 } f.myCall(obj,1,2) //否则this指向window obj.greet.call({name: 'Spike'}) //打出来的是 Spike
手写apply(arguments[this, [参数1,参数2.....] ])
Function.prototype.myApply=function(context){ // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数 let obj=context||window obj.fn=this const arg=arguments[1]||[] //若有参数,得到的是数组 let res=obj.fn(...arg) delete obj.fn return res } function f(a,b){ console.log(a,b) console.log(this.name) } let obj={ name:'张三' } f.myApply(obj,[1,2]) //arguments[1]
手写bind
this.value = 2 var foo = { value: 1 }; var bar = function(name, age, school){ console.log(name) // 'An' console.log(age) // 22 console.log(school) // '家里蹲大学' } var result = bar.bind(foo, 'An') //预置了部分参数'An' result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中
简单版本
Function.prototype.bind = function(context, ...outerArgs) { var fn = this; return function(...innerArgs) { //返回了一个函数,...rest为实际调用时传入的参数 return fn.apply(context,[...outerArgs, ...innerArgs]); //返回改变了this的函数, //参数合并 } }
new失败的原因:
例:
// 声明一个上下文 let thovino = { name: 'thovino' } // 声明一个构造函数 let eat = function (food) { this.food = food console.log(`${this.name} eat ${this.food}`) } eat.prototype.sayFuncName = function () { console.log('func name : eat') } // bind一下 let thovinoEat = eat.bind(thovino) let instance = new thovinoEat('orange') //实际上orange放到了thovino里面 console.log('instance:', instance) // {}
生成的实例是个空对象
在new
操作符执行时,我们的thovinoEat
函数可以看作是这样:
function thovinoEat (...innerArgs) { eat.call(thovino, ...outerArgs, ...innerArgs) }
在new操作符进行到第三步的操作thovinoEat.call(obj, ...args)
时,这里的obj
是new操作符自己创建的那个简单空对象{}
,但它其实并没有替换掉thovinoEat
函数内部的那个上下文对象thovino
。这已经超出了call
的能力范围,因为这个时候要替换的已经不是thovinoEat
函数内部的this
指向,而应该是thovino
对象。
换句话说,我们希望的是new
操作符将eat
内的this
指向操作符自己创建的那个空对象。但是实际上指向了thovino
,new
操作符的第三步动作并没有成功!
可new可继承版本
Function.prototype.bind = function (context, ...outerArgs) { let that = this; function res (...innerArgs) { if (this instanceof res) { // new操作符执行时 // 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{} that.call(this, ...outerArgs, ...innerArgs) } else { // 普通bind that.call(context, ...outerArgs, ...innerArgs) } } res.prototype = this.prototype //!!! return res }
9. 手动实现new
new的过程文字描述:
创建一个空对象 obj;
将空对象的隐式原型(proto)指向构造函数的prototype。
使用 call 改变 this 的指向
如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
function Person(name,age){ this.name=name this.age=age } Person.prototype.sayHi=function(){ console.log('Hi!我是'+this.name) } let p1=new Person('张三',18) ////手动实现new function create(){ let obj={} //获取构造函数 let fn=[].shift.call(arguments) //将arguments对象提出来转化为数组,arguments并不是数组而是对象 !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果 或者let arg = [].slice.call(arguments,1) obj.__proto__=fn.prototype let res=fn.apply(obj,arguments) //改变this指向,为实例添加方法和属性 //确保返回的是一个对象(万一fn不是构造函数) return typeof res==='object'?res:obj } let p2=create(Person,'李四',19) p2.sayHi()
细节:
[].shift.call(arguments) 也可写成: let arg=[...arguments] let fn=arg.shift() //使得arguments能调用数组方法,第一个参数为构造函数 obj.__proto__=fn.prototype //改变this指向,为实例添加方法和属性 let res=fn.apply(obj,arg)
10. 手写promise(常见promise.all, promise.race)
// Promise/A+ 规范规定的三种状态 const STATUS = { PENDING: 'pending', FULFILLED: 'fulfilled', REJECTED: 'rejected' } class MyPromise { // 构造函数接收一个执行回调 constructor(executor) { this._status = STATUS.PENDING // Promise初始状态 this._value = undefined // then回调的值 this._resolveQueue = [] // resolve时触发的成功队列 this._rejectQueue = [] // reject时触发的失败队列 // 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this) const resolve = value => { const run = () => { // Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled if (this._status === STATUS.PENDING) { this._status = STATUS.FULFILLED // 更改状态 this._value = value // 储存当前值,用于then回调 // 执行resolve回调 while (this._resolveQueue.length) { const callback = this._resolveQueue.shift() callback(value) } } } //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务) setTimeout(run) } // 同 resolve const reject = value => { const run = () => { if (this._status === STATUS.PENDING) { this._status = STATUS.REJECTED this._value = value while (this._rejectQueue.length) { const callback = this._rejectQueue.shift() callback(value) } } } setTimeout(run) } // new Promise()时立即执行executor,并传入resolve和reject executor(resolve, reject) } // then方法,接收一个成功的回调和一个失败的回调 function then(onFulfilled, onRejected) { // 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行 typeof onFulfilled !== 'function' ? onFulfilled = value => value : null typeof onRejected !== 'function' ? onRejected = error => error : null // then 返回一个新的promise return new MyPromise((resolve, reject) => { const resolveFn = value => { try { const x = onFulfilled(value) // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } } } const rejectFn = error => { try { const x = onRejected(error) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } switch (this._status) { case STATUS.PENDING: this._resolveQueue.push(resolveFn) this._rejectQueue.push(rejectFn) break; case STATUS.FULFILLED: resolveFn(this._value) break; case STATUS.REJECTED: rejectFn(this._value) break; } }) } catch (rejectFn) { return this.then(undefined, rejectFn) } // promise.finally方法 finally(callback) { return this.then(value => MyPromise.resolve(callback()).then(() => value), error => { MyPromise.resolve(callback()).then(() => error) }) } // 静态resolve方法 static resolve(value) { return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value)) } // 静态reject方法 static reject(error) { return new MyPromise((resolve, reject) => reject(error)) } // 静态all方法 static all(promiseArr) { let count = 0 let result = [] return new MyPromise((resolve, reject) => { if (!promiseArr.length) { return resolve(result) } promiseArr.forEach((p, i) => { MyPromise.resolve(p).then(value => { count++ result[i] = value if (count === promiseArr.length) { resolve(result) } }, error => { reject(error) }) }) }) } // 静态race方法 static race(promiseArr) { return new MyPromise((resolve, reject) => { promiseArr.forEach(p => { MyPromise.resolve(p).then(value => { resolve(value) }, error => { reject(error) }) }) }) } }
11. 手写原生AJAX
步骤
创建 XMLHttpRequest 实例
发出 HTTP 请求
服务器返回 XML 格式的字符串
JS 解析 XML,并更新局部页面
不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
version 1.0:
myButton.addEventListener('click', function () { ajax() }) function ajax() { let xhr = new XMLHttpRequest() //实例化,以调用方法 xhr.open('get', 'https://www.google.com') //参数2,url。参数三:异步 xhr.onreadystatechange = () => { //每当 readyState 属性改变时,就会调用该函数。 if (xhr.readyState === 4) { //XMLHttpRequest 代理当前所处状态。 if (xhr.status >= 200 && xhr.status <p>promise实现</p><pre class="brush:php;toolbar:false">function ajax(url) { const p = new Promise((resolve, reject) => { let xhr = new XMLHttpRequest() xhr.open('get', url) xhr.onreadystatechange = () => { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status console.log(res)) .catch(reason => console.log(reason))
12. 手写节流防抖函数
函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。
节流:连续触发事件但是在 n 秒中只执行一次函数
例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。
防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。
防抖的实现:
function debounce(fn, delay) { if(typeof fn!=='function') { throw new TypeError('fn不是函数') } let timer; // 维护一个 timer return function () { var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象) var args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout(function () { fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args); }, delay); }; } // 调用 input1.addEventListener('keyup', debounce(() => { console.log(input1.value) }), 600)
节流的实现:
function throttle(fn, delay) { let timer; return function () { var _this = this; var args = arguments; if (timer) { return; } timer = setTimeout(function () { fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments // fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。 timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器 }, delay) } } p1.addEventListener('drag', throttle((e) => { console.log(e.offsetX, e.offsetY) }, 100))
13. 手写Promise加载图片
function getData(url) { return new Promise((resolve, reject) => { $.ajax({ url, success(data) { resolve(data) }, error(err) { reject(err) } }) }) } const url1 = './data1.json' const url2 = './data2.json' const url3 = './data3.json' getData(url1).then(data1 => { console.log(data1) return getData(url2) }).then(data2 => { console.log(data2) return getData(url3) }).then(data3 => console.log(data3) ).catch(err => console.error(err) )
14. 函数实现一秒钟输出一个数
for(let i=0;i{ console.log(i); },1000*i) }
15. 创建10个标签,点击的时候弹出来对应的序号?
var a for(let i=0;i' a.addEventListener('click',function(e){ console.log(this) //this为当前点击的<a> e.preventDefault() //如果调用这个方法,默认事件行为将不再触发。 //例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。 alert(i) }) const d=document.querySelector('p') d.appendChild(a) //append向一个已存在的元素追加该元素。 }</a>