本文主要记录了关于前端面试常考的手写代码题,常看常复习。
JS 基础篇
深入了解 JS 这门语言的运行逻辑与机制,尝试实现其内部的方法。
ES5 实现继承
使用 ES5 的语法实现继承,有几个注意点:
- 使用
Object.create
方法,构造以父类的prototype
为原型的新对象,防止对子类原型对象的修改影响到父类的原型对象。
- 注意静态方法的继承,使用
Object.setPrototypeOf
将父类设置为子类的原型。
验证的例子:
实现 new 函数
首先对问题进行分析,分析new
关键字到底干了什么。
- 创建一个空对象将该对象的原型设置为函数的
prototype
。
- 执行函数,
this
指向指向该对象。
- 若函数返回结果不为空且为对象,则返回该对象,否则返回先前生成的对象。
手写 instanceof
沿着原型链循环查找,即寻找当前对象实例的__proto__
的__proto__
,是否等于构造函数的prototype
。
手写数据类型判断函数
Object.prototype.toString
调用与正则,需要注意的是call
方法的调用与match
方法的返回结果。
手写 promisify 函数
- 注意
promisify
需要返回一个函数,这个函数返回一个promise
对象。
注意 Node.js 中常规的回调函数形式,因此在改变this
执行的时候使用call
方法代码更加简洁。
err
的情况就先reject
,也是利用了回调函数的特性。
手写 async 方法
async
与await
的语法其实是generator
函数与promise
结合的语法糖,通过promise
实现自动执行最后实现协程的效果。
首先查看 generator
函数的基本使用
手写 EventEmitter 函数
- 注意
emit
回调函数的this
指向以及参数的问题。
- 注意
once
函数可以将函数包装成另一个执行函数,在执行完这个函数后就移除掉。
手写 call,apply,bind
注意自己实现需要实现利用对象的方法隐式绑定做到的,注意bind
关键字作为new
方法调用的时候对this
指向做出特殊的。
super
关键字可以作为构造函数在构造函数里面调用super
关键字还做作为对象调用父对象上的方法或者静态方法,但在普通方法里面只能调用普通方法而不能调用静态方法,但在子类的静态方法中则可以都可以调用。
手写 Object.create
利用new
关键字,实例的__proto__
属性会指向构造函数的prototype
。
- 在
new
初始化一个对象的规范:如果构造函数的prototype
不是一个对象,则将新创建的对象的__proto__
设置为标准的内置的对象的原型对象,即Object.prototype
。
- 在使用
Object.create
方法时,强制把这个对象的__proto__
设置为该参数。目前该方法是将对象原型设置为空的唯一手段。
Class
类的prototype
属性默认不可写。
手写数组 push
这种方法有点取巧,很多特性都是 JS 数组特有的行为,比如数组长度会随之数组的最大索引变化,没有值的数据会自动填充为空。
手写字符串 repeat
实现字符串的repeat
方法,注意递归的核心为保证字符串不变
手写 Object.assign
注意对参数为剩余参数,同时Object
强制转换对象会返回对象本身。
实现数组的 flat 方法
核心思路为递归,使用数组的reduce
方法能减少不少代码量。
深入 JS 的使用
柯里化
注意外层函数需要命名以便在递归的时候引用,内层函数则是内部也应该返回函数的相关结果。
深拷贝
采用ES6
之后的方法进行,原型链与属性修饰符
Descriptors
来全给你拷了。
采用for
循环实现深拷贝
防抖与节流
防抖的复杂实现注意isInvoked
的位置放在了返回函数的里面,这样可以保证初始执行的这一次不会触发trailing
这一次的执行。有定时器就清除,无论之前有没有定时器都需要继续设置setTimeout
。
节流的复杂实现在于实现trailing
的功能,这边涉及到递归函数的提取。注意之前有定时器的话就只保存参数什么都不做,没定时器才要设置setTimeout
。
并发控制
并发池
递归版本,也叫能够实现并发控制的Promise.all
版本。注意while
循环的巧妙使用,先将能填进去的数组先填进去。为了严格与Promise.all
方法保持一致,这里做到了索引与数组直接相同。同时还需要注意递归调用的重要性。同时还有万能并发池的版本,有点缺陷的地方在于数组splice
方法的时间复杂度部分。
调度器版本
难绷,曾经最熟悉的一题,居然没做出来。🤡 卡点主要在于add
方法需要返回一个promise
对象,这个promise
对象的状态应该由task
函数的执行结果来决定,但根据并发控制的需求,不能直接执行task
函数,这样违背了并发控制的需求,需要将这个函数再包装一下,包装成另一个函数即() => task().then(resolve, reject)
,推入任务队列,其他即为递归的调度执行。注意由于任务是一个个加进来的所以使用的是if
判断条件,和上面的还是有一些不一样。
lodash.get
基本思路为解析路径至数组然后使用数组的reduce
方法进行递归获取对象
lodash.set
这个比较复杂一些,需要注意的细节有点多。
千分位格式化
两种方式,循环与正则,推荐正则,注意正先行断言-存在使用的一些注意事项。
LazyMan
很经典的手写题,值得注意的地方有:
- 维护一个
tasks
队列存储所有任务。
- 使用
setTimeout
函数将任务推入宏任务队列,并且使用async
和await
语法配合for
循环控制任务的执行顺序。
- 返回一个对象,对象身上有相关的执行方法,每个执行方法都返回对象本身,实现链式调用,注意相关方法不能使用箭头函数,这样
this
指向会出现问题,不会指向返回的对象。
数组转树
JSON
文件转化为树结构的问题,关键在于利用对象的引用和map
结构来进行操作。
面试的时候这么简单的题居然没做出来,顶级 🤡,DFS
入脑了看啥都想DFS
,其实只要对每个对象建立一个Map
,然后遍历对象进行赋值就好了,主要还是利用了对象本身的唯一性。
斐波那契数列的两种实现方式
尾调用优化解决问题。
业务场景题
利用迭代器来使async
与await
来阻塞当前函数的执行线程。
使用promise
封装一个ajax
请求
实现日期格式化函数
交换a, b
的值不能用临时变量
注意ajax
一定需要send
方法
数组的乱序输出
实现数组的斜向打印
电话号码的打印:
循环打印方案
丢手帕问题
查找文章中出现频率最高的单词
setTimeout
模仿setInterval
判断对象中是否存在循环引用
手写一个undefinedToNull
函数
判断字符串的有效数字
实现一个累加器
counter
function
自执行包裹
counter
对象,简单的数据代理
失败后自动发起请求,超时后停止。
封装一个fetch
请求