同事问我,最近前端面试手写考什么
前言
最近有同事出去看机会,问我最近前端面试问什么,于是我帮他整理了一波最近比较火的前端手写面试题。
new的原理
new是javascript的一个关键词,用来创建对象的实例,它的实现思路如下:
- 创建一个空对象obj
- 将obj象的原型指向构造函数的prototype
- 执行构造函数,将obj作为其运行时的this
- 如果构造函数返回了一个对象,则用这个对象作为new的结果,反之则返回obj
function myNew(fn, ...args) {
const obj = Object.create(fn.prototype)
const res = fn.apply(obj, args)
return res instanceof Object ? res : obj
}
instanceof原理
instanceof的主要作用是判断某个实例是否属于某种类型,也可以判断一个实例是否是其父类型或者祖先类型的实例。
实现思路:通过原型链往上一级一级查找,直到找到和当前对象的原型相等的原型。
function my_instance_of(target, obj) {
let proto = Object.getPrototypeOf(target)
while(proto !== null) {
if (proto === obj.prototype) return true
proto = Object.getPrototypeOf(proto)
}
return false
}
另外再提一下:
Object instanceof Object
结果为true
。因为Object.__proto__
为Function.prototype
,Function.prototype.__proto__
为Object.prototype
Foo instance Foo
结果为false
。因为Foo.__proto__
为Function.prototype
,而Foo的原型上没有Function.prototype
Object.create()原理
实现思路:传入一个对象,以该对象作为原型构造出一个新对象。
function create(proto) {
function Temp() {}
Temp.prototype = proto
return new Temp()
}
实现一个发布订阅模式 EventEmitter
实现思路:
- 通过初始化一个events对象保存事件和监听函数的映射关系
- on:监听事件,将事件名eventName和回调函数 fn 存入events中
- emit:触发事件,通过eventName事件名取出对应的回调函数并执行
- off:取消监听,通过eventName事件名和找到对应的回调函数fn并将其移除
- once:只触发一次,在触发fn时,调用off方法将其移除
class EventEmitter {
constructor() {
this.events = {}
}
on(eventName, fn) {
this.events[eventName] ? this.events[eventName].push(fn) : (this.events[eventName] = [fn])
return this
}
emit(eventName) {
(this.events[eventName] || []).forEach(fn => fn())
return this
}
off(eventName, fn) {
if (this.events[eventName]) {
this.events[eventName] = fn ? this.events[eventName].filter(item => item !== fn) : []
}
return this
}
once(eventName, callback) {
const fn = () => {
callback()
this.off(eventName, fn)
}
this.on(eventName, fn)
}
}
reduce原理
实现思路:
- 如果传入了初始值 initialValue,那么就将此值作为调用回调函数的第一个入参,然后循环的索引 i 从0开始
- 未传入初始值 initialValue,将数组的第一项作为 initialValue,然后循环的索引 i 从1开始
- 从 i 开始循环到 数组的length - 1,将上一次的输出作为下一次循环的第一个参数
Array.prototype.myReduce = function (fn, initialValue) {
let [accumulator, i] = initialValue !== undefined ? [initialValue, 0] : [this[0], 1]
for(; i < this.length; i++) {
accumulator = fn(accumulator, this[i], i, this)
}
return accumulator
}
call/apply/bind原理
call、apply和bind都是用于改变函数this,call和apply的区别是传入的参数上有差异,apply只有两个参数,第二个参数是一个数组,call则是可以传入N个参数,第二个参数到第N个参数会当做参数传递给调用的fn,而bind与它们的区别是bind是静态绑定 。
实现思路:
- bind:将当前函数赋值给传入的上下文context的某个属性,比如fn,然后通过context.fn调用
- apply:除了参数的处理不一样,其它和bind相同
- bind: 返回一个新函数,在新函数里面调用原函数,并将this指向它
// call
Function.prototype.myCall = function (context, ...args){
context = context || window
context.fn = this
const res = context.fn(...args)
delete context.fn
return res
}
// apply
Function.prototype.myApply = function (context, arg){
context = context || window
context.fn = this
let res
if (Array.isArray(arg)) {
res = context.fn(...arg)
} else {
res = context.fn()
}
delete context.fn
return res
}
// bind
// 注意:this的优先级new > bind
Function.prototype.myBind = function (context = window, ...outer) {
const _this = this
return function callback(...inner) {
_this.call(context, ...outer, ...inner)
if (this instanceof callback) {
return new _this(...inner)
}
_this.call(context, ...outer, ...inner)
}
}
函数柯里化
先介绍一下什么是函数柯里化:函数柯里化是一种函数转换的技术,它可以将接收多个参数的函数,转化为一系列接收单个参数的函数 。
如何实现add(1)(2, 3)(4)() === 10 ?
实现思路:传入单个参数时,将参数存起来,当不传入参数时,需要将原函数进行执行。
function currying(fn) {
let allArgs = []
return function next(...args) {
if (args.length > 0) {
allArgs = allArgs.concat(args)
return next
}
return fn.apply(null, allArgs)
}
}
const add = currying(function(...args){
return args.reduce((accu, current) => {
return accu + current
}, 0)
});
// 打印10
console.log(add(1)(2, 3)(4)())
扩展:如何实现add(1)(2, 3)(4)(5) == 15?
这里需要涉及到js的取值规则了,js在获取变量值的时候会在恰当时机隐式调用Symbol.toPrimitive、valueOf以及toString方法。具体实现看如下代码:
function currying(fn) {
let allArgs = []
function next(...args) {
allArgs = allArgs.concat(args)
return next
}
// 方法1:定义Symbol.toPrimitive
next[Symbol.toPrimitive] = function () {
return fn.apply(null, allArgs)
}
// 方法2: 定义valueOf和toString方法
// 字符类型
next.toString = function(){
console.log('toString')
return fn.apply(null, allArgs);
};
// 数值类型
next.valueOf = function(){
console.log('valueOf')
return fn.apply(null, allArgs);
}
return next
}
const add = currying(function (...args) {
return args.reduce((accu, current) => {
return accu + current
}, 0)
})
// 验证:输出true
console.log(add(1)(2, 3)(4)(5) == 15)
反柯里化
函数的反柯里化指的是将函数的适用范围进行扩大。
比如有一个Animal类,想要借助其getName方法,输出另一个对象Person的内容。
function Animal(name) {
this.name = name
}
Animal.prototype.getName = function () {
return this.name
}
const Person = {
name: 'zs'
}
第一种实现:
Function.prototype.unCurrying = function () {
const _this = this
return function (that, ...args) {
return _this.apply(that, args)
}
}
const getName = Animal.prototype.getName.unCurrying();
console.log(getName(Person));
第二种实现:
Function.prototype.unCurrying = function () {
const _this = this
return function () {
return Function.prototype.call.apply(_this, arguments)
}
}
const getName = Animal.prototype.getName.unCurrying()
console.log(getName(Person))
防抖
函数在一定时间内被重复调用多次,只会执行一次。
function debounce(fn, delay) {
let timer = null
return function () {
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
}, delay)
}
}
节流
函数被调用多次,一段时间内只会执行一次。
function throttle(fn, delay) {
let prevTime = Date.now()
return function () {
if (Date.now() - prevTime > delay) {
prevTime = Date.now()
fn.apply(this, arguments)
}
}
}
数组扁平化
数组原型上的flat可以实现扁平化,其参数为嵌套层级,传入Infinity可以扁平化任意层级的数组,但兼容性较差,所以需要手动实现。
- 使用递归
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
- 使用循环
function flatten(arr) {
const res = []
while(arr.length) {
const item = arr.shift()
Array.isArray(item) ? arr.unshift(...item) : res.push(item)
}
return res
}
- 使用eval
function flatten(arr) {
return eval(`[${arr}]`)
}
compose
compose能将一系列函数组合起来,在调用时,从右往左进行调用,并将上一次调用返回的结果作为下一次调用的入参。
function compose(...fns) {
if (!fns.length) return (v) => v
if (fns.length === 1) return fn[0]
return fns.reduce((pre, cur) => (...args) => pre(cur(...args)) )
}
partial 偏函数
它能够先固化一部分参数,达到设置默认值的目的,然后返回一个新函数。
function partial(f, ...args) {
return (...moreArgs) => f(...args, ...moreArgs)
}
deepClone
对象的深拷贝,主要需要注意用 WeakMap 处理循环引用问题,递归拷贝。
const isObj = (target) => typeof target === 'object' && target !== null
function deepClone(obj, hash = new WeakMap()) {
if (!isObj(obj)) return obj
if (hash.has(obj)) return has.get(obj)
const target = new obj.constructor()
hash.set(obj, target)
Object.keys(obj).forEach((key) => {
target[key] = deepClone(obj[key], hash)
})
return target
}
map
数组的map方法。
function map(arr, fn) {
let idx = 0,
len = arr.length,
result = new Array(len)
while(idx < len) {
result[idx] = fn(arr[idx], idx, arr)
idx++
}
return result
}
reduce
数组的reduce方法。
function reduce(arr, fn, accumulator) {
let idx = -1, len = arr.length
if (accumulator === undefined && len > 0) {
accumulator = arr[++idx]
}
while(++idx < len) {
accumulator = fn(accumulator, arr[idx], idx, arr)
}
return accumulator
}
filter
数组的filter方法。
function filter(arr, fn) {
let idx = -1, len = arr.length, result = []
while(++idx < len) {
fn(arr[idx], idx, arr) && (result.push(arr[idx]))
}
return result
}
LRU缓存算法
LRU是一种缓存替换策略,当缓存空间已满时,会移除最近最少使用的项目。
class LRUCache {
constructor(limit = 5) {
this.map = new Map()
this.limit = limit
}
add(key, value) {
if (this.map.get(key)) {
this.map.delete(key)
} else if (this.map.size === this.limit) {
this.map.delete(this.map.keys().next().value)
}
this.map.set(key, value)
}
get(key) {
if (this.map.get(key)) {
const res = this.map.get(key)
this.map.delete(key)
this.map.set(key, res)
return res
}
return -1
}
}
小结
以上就是我总结的关于前端面试常见的手写题,难度虽然不大,但在面试时一旦出现紧张,还是很容易遗忘部分关键点的,所以平时还是要多加积累,常复习,以上有一些比如防抖、节流只是写了最简单的版本,面试的时候可能会考的更加深入,所以需要针对性的进行深入的一些准备,另外祝每个小伙伴都能拿到满意的offer~
转载自:https://juejin.cn/post/7367278179785801782