likes
comments
collection
share

同事问我,最近前端面试手写考什么

作者站长头像
站长
· 阅读数 23

前言

最近有同事出去看机会,问我最近前端面试问什么,于是我帮他整理了一波最近比较火的前端手写面试题。

new的原理

new是javascript的一个关键词,用来创建对象的实例,它的实现思路如下:

  1. 创建一个空对象obj
  2. 将obj象的原型指向构造函数的prototype
  3. 执行构造函数,将obj作为其运行时的this
  4. 如果构造函数返回了一个对象,则用这个对象作为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.prototypeFunction.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

实现思路:

  1. 通过初始化一个events对象保存事件和监听函数的映射关系
  2. on:监听事件,将事件名eventName和回调函数 fn 存入events中
  3. emit:触发事件,通过eventName事件名取出对应的回调函数并执行
  4. off:取消监听,通过eventName事件名和找到对应的回调函数fn并将其移除
  5. 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原理

实现思路:

  1. 如果传入了初始值 initialValue,那么就将此值作为调用回调函数的第一个入参,然后循环的索引 i 从0开始
  2. 未传入初始值 initialValue,将数组的第一项作为 initialValue,然后循环的索引 i 从1开始
  3. 从 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.toPrimitivevalueOf以及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可以扁平化任意层级的数组,但兼容性较差,所以需要手动实现。

  1. 使用递归
function flatten(arr) {  
  return arr.reduce((result, item)=> {
      return result.concat(Array.isArray(item) ? flatten(item) : item);
  }, []);
}
  1. 使用循环
function flatten(arr) {
  const res = []
  while(arr.length) {
    const item  = arr.shift()
    Array.isArray(item) ? arr.unshift(...item) :  res.push(item)
  }
  return res
}
  1. 使用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
评论
请登录