前端手写面试题

140 阅读6分钟

1. 手写一个JS函数,实现数组扁平化Array Flatten

1.1只减少一级嵌套

如输入[1,[2,[3]],4] 返回[1,2,[3],4]

/*
  思路一:利用push
      定义空数组result=[],遍历当前数组
      如果item是数组,则累加到result
      如果item不是数组,则遍历之后累加到result
**/
function ArrayFlatten(array) {
    const result = [];
    array.forEach((item, index) => {
        if(Array.isArray(item)) {
            item.forEach(item => result.push(item));
        } else {
            result.push(item);
        }
    })
    return result;
}
// 思路二:利用concat(item: any[] | any ):arr 不影响原数组
function ArrayFlatten(array) {
    let result = [];
    array.forEach((item, index) => {
        result = result.concat(item);
    })
    return result;
}

1.2深度扁平化

// 先实现一级扁平化,然后递归调用,知道全部拍平
function deepFlatten(array) {
    let res = [];
    function fn(arr) {
        arr.forEach(item => {
            if(Array.isArray(item)) {
                fn(item);
            } else {
                res.push(item);
            }
        })
    }
    fn(array);
    return res;
}
// 利用concat
function deepFlatten(arr) {
    let res = [];
    arr.forEach(item => {
        if (Array.isArray(item)) {
            const flatItem = deepFlatten(item);
            res = res.concat(flatItem);
        } else {
            res = res.concat(item);
        }
    })
    return res;
}
arr.join(',').split(',')和arr.toString().split(',')
对于数组有引用对象,像{x: 1}这种就不适用了,不是完美方案

对象扁平化

function objectFlat(obj = {}) {
  const res = {}
  function flat(item, preKey = '') {
    Object.entries(item).forEach(([key, val]) => {
      const newKey = preKey ? `${preKey}.${key}` : key
      if (val && typeof val === 'object') {
        flat(val, newKey)
      } else {
        res[newKey] = val
      }
    })
  }
  flat(obj)
  return res
}

// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } }
console.log(objectFlat(source));

2.getType

手写一个getType函数,无论传入任意变量,可准确获取类型,包括number、string、boolean等值类型,还有object、array、map、regexp等引用类型

1、typeof 能判断值类型,function和object,像map、set这些都判断成object,有局限性,且typeof null也是object

2、instanceof需要两个参数去判断,而不是获取类型

// 利用Object.prototype.toString.call
function getType(x) {
    const originType = Object.prototype.toString.call(x);
    const spaceIndex = originType.indexOf(' ');
    const type = originType.slice(spaceIndex + 1, -1);
    return type.toLowerCase();
} 

3、手写new

new 的过程: 创建一个空对象obj,继承构造函数的原型 执行构造函数(将obj作为this) 返回obj

function customNew<T>(constructor: Function, ...args: any[]): T {
    // 1、创建空对象,继承构造函数constructor原型
    var obj = Object.create(constructor.prototype);
    // 2、将obj作为this,执行constructor,传入参数
    const res = constructor.apply(obj, args);
    // 3、返回obj
    if (typeof res === "function" || (typeof res === "object" && res !== null)) {
        return res
    }
    return obj;
}
// 测试
function Foo(name, n) {
    this.name = name;
    this.city = '北京';
    this.n = n;
}
Foo.prototype.getName = function () {
    return this.name;
}

const obj = customNew<Foo>(Foo, '小明', 18)

Object.create 和{}区别: Object.create创建的空对象原型链指向传入的参数 {}.proto===Object.prototype 遍历index

防抖节流

防抖
function debounce(fn, delay = 300) {
  let timer = null;
  return function(...args) {
    if(timer) {
      clearTimeout(timer);
      timer = null;
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  }
}
节流
function throttle(fn, delay) {
  let timer = 0;
  return function(...args) {
    if(timer) return;
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  }
}

lazyMan

支持sleep和eat,链式调用

const me = new LazyMan('小明');
me.eat('苹果').eat('香蕉').sleep(5).eat('葡萄'); // 打印结果如下
// '小明eat苹果'
// '小明eat香蕉'
// 等待5s
// '小明eat葡萄'

由于有sleep功能,函数不能直接在调用时触发 初始化一个列表,把函数注册进去 由每一个item触发next执行(遇到sleep则异步调用)

class LazyMan {
    constructor(name) {
        this.name = name;
        this.list = [];
        setTimeout(() => {
            this.next();
        }, 0)
    }
    next() {
        if (this.list.length) {
            const task = this.list.shift();
            task();
        }
    }
    eat(val) {
        let task = () => {
            console.log(`${this.name}${val}`)
            this.next();
        };
        this.list.push(task);
        return this;
    }
    sleep(time) {
        let task = () => {
            setTimeout(() => {
                this.next();
            }, time*1000)
        };
        this.list.push(task);
        return this;
    }
}

也可以用promise来实现

class LazyMan {
    constructor(name) {
        this.name = name;
        this.pro = Promise.resolve();
    }
    eat(val) {
        this.pro = this.pro.then(() => {
            console.log(`${this.name}${val}`);
        })
        return this;
    }
    sleep(time) {
        this.pro = this.pro.then(() => {
            return new Promise((res)=> {
                setTimeout(() => {
                    res();
                }, time*1000)
            })
        })
        return this;
    }
}

手写curry

function curry(fn) {
    //...
}
function add(a,b,c) {
    return a + b + c;
}
const curryAdd = curry(add);
console.log(curryAdd(1)(2)(3)); // 6

curry返回的是一个函数,

function curry(fn) {
    let fnArgsLength = fn.length;//传入的函数形参长度
    let args = [];
    function calc() {
    // 积累参数
        args = [...args, ...arguments];
        if (args.length < fnArgsLength) {
            // 参数不够,返回函数
            return calc;
        } else {
            // 参数够了,执行函数
            return fn.apply(this, args.slice(0, fnArgsLength);
        }
    }
    return calc;
}

手写instanceof

function myInstanceOf(left, right) {
    if (left == null) return false; // null undefined 返回false
    const type = typeof left; // 基础数据类型返回false
    if (type !== 'object' && type !== 'function') {
        return false;
    }
    let tempInstance = left // 为了防止修改left
    while (tempInstance) {
        if(tempInstance === right.prototype) {
            return true;
        }
        tempInstance = tempInstance.__proto__;
    }
    return false;
}

手写bind

返回新函数 绑定this 同时绑定执行时的参数(apply或call)

Function.prototype.myBind = function (context = globalThis) {
  const fn = this
  const args = Array.from(arguments).slice(1)
  const newFunc = function () {
    const newArgs = args.concat(...arguments)
    if (this instanceof newFunc) {
      // 通过 new 调用
      return new fn(...newArgs)
    } else {
      // 通过普通函数形式调用,绑定 context
      return fn.apply(context, newArgs)
    }
  }
  newFn.prototype = Object.create(fn.prototype);
  return newFunc;
}
var fn = function(a, b, c){
    console.log(this, a, b, c);
}
var a = fn.myBind({}, 1,2)
a(3)
// {} 1 2 3

// 支持new
const me = { name: 'Jack' }
function say() {
  console.log(`My name is ${this.name || 'default'}`);
}
const meSay = say.myBind(me)
const obj = new meSay();
obj.__proto__ === say.prototype;// true

手写apply,call

如const obj = {x: 100,fn(){this.x}} 执行obj.fn(),此时fn的内部this指向obj 可借此来实现函数绑定this

Function.prototype.customCall = function(context, ...args) {
    if (context == null) context = globalThis; // fn.call(null)时,浏览器环境下相当于fn.call(window)
    if (typeof context !== 'object') context = new Object(context); // 值类型,变为
    const fnKey = Symbol() // 不会出现属性名称覆盖
    context[fnKey] = this; // this指向当前的函数

    var res = context[fnKey](...args); // 绑定this

    delete context[fnKey]; // 清理掉 fn,防止污染
    return res;
}
var fn = function(a, b, c){
    console.log(this, a, b, c);
}
fn.customCall({}, 1,2,3)
Function.prototype.customApply = function(context, args = []) {
    if (context == null) context = globalThis; // fn.call(null)时,浏览器环境下相当于fn.call(window)
    if (typeof context !== 'object') context = new Object(context); // 值类型,变为对象
    const fnKey = Symbol() // 不会出现属性名称覆盖
    context[fnKey] = this; // this指向当前的函数

    var res = context[fnKey](...args); // 绑定this

    delete context[fnKey]; // 清理掉 fn,防止污染
    return res;
}
  
var fn = function(a, b, c){
    console.log(this, a, b, c);
}
fn.customApply({}, [1,2,3])

#深拷贝

function deepCopy(obj, cache = new WeakMap()) {
  if (!obj instanceof Object) return obj
  // 防止循环引用
  if (cache.get(obj)) return cache.get(obj)
  // 支持函数
  if (obj instanceof Function) {
    return function () {
      obj.apply(this, arguments)
    }
  }
  // 支持日期
  if (obj instanceof Date) return new Date(obj)
  // 支持正则对象
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
  // 还可以增加其他对象,比如:Map, Set等,根据情况判断增加即可,面试点到为止就可以了

  // 数组是 key 为数字素银的特殊对象
  const res = Array.isArray(obj) ? [] : {}
  // 缓存 copy 的对象,用于处理循环引用的情况
  cache.set(obj, res)

  Object.keys(obj).forEach((key) => {
    if (obj[key] instanceof Object) {
      res[key] = deepCopy(obj[key], cache)
    } else {
      res[key] = obj[key]
    }
  });
  return res
}

// 测试
const source = {
  name: 'Jack',
  meta: {
    age: 12,
    birth: new Date('1997-10-10'),
    ary: [1, 2, { a: 1 }],
    say() {
      console.log('Hello');
    }
  }
}
source.source = source
const newObj = deepCopy(source)
console.log(newObj.meta.ary[2] === source.meta.ary[2]); // false
console.log(newObj.meta.birth === source.meta.birth); // false

eventBus

on和once注册函数,存储起来、 emit时找到对应的函数,执行 off找到对应函数,从对象中删除

class eventBus {
    /**
     * {
     *      'key1': [{
     *          fn: function,
     *          isOnce: false
     *      }],
     *      'key2': [],
     *      'key3': []
     * }
     */
    private events: {
        [key: string]: Array<{fn: Function; isOnce: Boolean}>
    }

    constructor() {
        this.events = [];
    }

    on(type: string, fn: Function, isOnce: boolean = false) {
        const events = this.events;
        if(events[type] == null) {
            events[type] = [];
        }
        events[type].push({ fn, isOnce });
    }

    once(type: string, fn: Function) {
        this.on(type, fn, true);
    }

    off(type: string, fn?: Function) {
        if (!fn) {
            // 解绑所有type的函数
            this.events[type] = [];
        } else {
            // 解绑单个函数
            const fnList = this.events[type]
            if (fnList) {
                this.events[type] = fnList.filter(item => item.fn !== fn)
            }
        }
    }
    
    emit(type: string, ...args: any[]) {
        const fnList = this.events[type]
        if (fnList) {
            this.events[type] = fnList.filter(item => {
                const { fn, isOnce} = item;
                fn(...args);
                // once 执行一次就要被过滤掉
                return !isOnce;
            })
        }
    }
}
const e = new eventBus();
function fn1(a, b) {console.log('fn1', a, b)}
function fn2(a, b) {console.log('fn2', a, b)}
function fn3(a, b) {console.log('fn3', a, b)}

e.on('key1', fn1)
e.on('key1', fn2)
e.once('key1', fn3)
e.on('xxx', fn3)
e.emit('key1', 10, 20) // 出发fn1 fn2 fn3
e.emit('key1', 11, 21)
e.off('key1', fn1)
e.emit('key1', 100, 200) // 触发fn2

es5实现继承

function create(proto) {
  const type = typeof proto
  const isObject = type === 'function' || type === 'object' && !!proto
  if (!isObject) return {}
  function F() {}
  F.prototype = proto;
  return new F();
}

// Parent
function Parent(name) {
  this.name = name
}

Parent.prototype.sayName = function () {
  console.log(this.name)
};

// Child
function Child(age, name) {
  Parent.call(this, name) // 执行父类的构造函数
  this.age = age
}
Child.prototype = create(Parent.prototype) // 子类原型链接父类原型
Child.prototype.constructor = Child // 子类原型constructor指向构造函数

Child.prototype.sayAge = function () {
  console.log(this.age)
}

// 测试
const child = new Child(18, 'Jack')
child.sayName()
child.sayAge()

实现LRU缓存

class LRUCache {
    constructor(length) {
        if (this.length < 1) throw new Error('invalid length');
        this.length = length;
        this.data = new Map()
    }
    set(key, val) {
        const data = this.data;
        if(data.has(key)) {
            data.delete(key);
        }
        data.set(key, val);
        if (data.size > this.length) {
            // 如果超出了容量,则删除Map最老的元素
            const delKey = data.keys().next().value;
            data.delete(delKey);
        }
    }
    get(key) {
        const data = this.data;

        if (!data.has(key)) return null;

        const value = data.get(key);

        data.delete(key);
        data.set(key, value);

        return value;
    }
}
const lruCache = new LRUCache(2);
lruCache.set(1, 1);
lruCache.set(2, 2);
console.log(lruCache.get(1));// 1{2=2,1=1}
lruCache.set(3, 3); // {1=1,3=3}
console.log(lruCache.get(2));// null{1=1,3=3}
lruCache.set(4, 4);// {3=3, 4=4}
console.log(lruCache.get(1)); // null {3=3, 4=4}
console.log(lruCache.get(3)); // 3 {4=4, 3=3}
console.log(lruCache.get(4)); // 4 {3=3, 4=4}

异步并发数限制

/**
 * 关键点
 * 1. new promise 一经创建,立即执行
 * 2. 使用 Promise.resolve().then 可以把任务加到微任务队列,防止立即执行迭代方法
 * 3. 微任务处理过程中,产生的新的微任务,会在同一事件循环内,追加到微任务队列里
 * 4. 使用 race 在某个任务完成时,继续添加任务,保持任务按照最大并发数进行执行
 * 5. 任务完成后,需要从 doingTasks 中移出
 */
function limit(count, array, iterateFunc) {
  const tasks = []
  const doingTasks = []
  let i = 0
  const enqueue = () => {
    if (i === array.length) {
      return Promise.resolve()
    }
    const task = Promise.resolve().then(() => iterateFunc(array[i++]))
    tasks.push(task)
    const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1))
    doingTasks.push(doing)
    const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve()
    return res.then(enqueue)
  };
  return enqueue().then(() => Promise.all(tasks))
}

// test
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i))
limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => {
  console.log(res)
})

异步串行 | 异步并行

// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {
  setTimeout(function () {
    callback(null, a + b);
  }, 500);
}

// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
  asyncAdd(a, b, (err, res) => {
    if (err) {
      reject(err)
    } else {
      resolve(res)
    }
  })
})

// 2. 串行处理
async function serialSum(...args) {
  return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}

// 3. 并行处理
async function parallelSum(...args) {
  if (args.length === 1) return args[0]
  const tasks = []
  for (let i = 0; i < args.length; i += 2) {
    tasks.push(promiseAdd(args[i], args[i + 1] || 0))
  }
  const results = await Promise.all(tasks)
  return parallelSum(...results)
}

// 测试
(async () => {
  console.log('Running...');
  const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res1)
  const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res2)
  console.log('Done');
})()

vue reactive

// Dep module
class Dep {
  static stack = []
  static target = null
  deps = null
  
  constructor() {
    this.deps = new Set()
  }

  depend() {
    if (Dep.target) {
      this.deps.add(Dep.target)
    }
  }

  notify() {
    this.deps.forEach(w => w.update())
  }

  static pushTarget(t) {
    if (this.target) {
      this.stack.push(this.target)
    }
    this.target = t
  }

  static popTarget() {
    this.target = this.stack.pop()
  }
}

// reactive
function reactive(o) {
  if (o && typeof o === 'object') {
    Object.keys(o).forEach(k => {
      defineReactive(o, k, o[k])
    })
  }
  return o
}

function defineReactive(obj, k, val) {
  let dep = new Dep()
  Object.defineProperty(obj, k, {
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })
  if (val && typeof val === 'object') {
    reactive(val)
  }
}

// watcher
class Watcher {
  constructor(effect) {
    this.effect = effect
    this.update()
  }

  update() {
    Dep.pushTarget(this)
    this.value = this.effect()
    Dep.popTarget()
    return this.value
  }
}

// 测试代码
const data = reactive({
  msg: 'aaa'
})

new Watcher(() => {
  console.log('===> effect', data.msg);
})

setTimeout(() => {
  data.msg = 'hello'
}, 1000)

promise

// 建议阅读 [Promises/A+ 标准](https://promisesaplus.com/)
class MyPromise {
  constructor(func) {
    this.status = 'pending'
    this.value = null
    this.resolvedTasks = []
    this.rejectedTasks = []
    this._resolve = this._resolve.bind(this)
    this._reject = this._reject.bind(this)
    try {
      func(this._resolve, this._reject)
    } catch (error) {
      this._reject(error)
    }
  }

  _resolve(value) {
    setTimeout(() => {
      this.status = 'fulfilled'
      this.value = value
      this.resolvedTasks.forEach(t => t(value))
    })
  }

  _reject(reason) {
    setTimeout(() => {
      this.status = 'reject'
      this.value = reason
      this.rejectedTasks.forEach(t => t(reason))
    })
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.resolvedTasks.push((value) => {
        try {
          const res = onFulfilled(value)
          if (res instanceof MyPromise) {
            res.then(resolve, reject)
          } else {
            resolve(res)
          }
        } catch (error) {
          reject(error)
        }
      })
      this.rejectedTasks.push((value) => {
        try {
          const res = onRejected(value)
          if (res instanceof MyPromise) {
            res.then(resolve, reject)
          } else {
            reject(res)
          }
        } catch (error) {
          reject(error)
        }
      })
    })
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

// 测试
new MyPromise((resolve) => {
  setTimeout(() => {
    resolve(1);
  }, 500);
}).then((res) => {
    console.log(res);
    return new MyPromise((resolve) => {
      setTimeout(() => {
        resolve(2);
      }, 500);
    });
  }).then((res) => {
    console.log(res);
    throw new Error('a error')
  }).catch((err) => {
    console.log('==>', err);
  })

图片懒加载

// <img src="default.png" data-src="https://xxxx/real.png">
function isVisible(el) {
  const position = el.getBoundingClientRect()
  const windowHeight = document.documentElement.clientHeight
  // 顶部边缘可见
  const topVisible = position.top > 0 && position.top < windowHeight;
  // 底部边缘可见
  const bottomVisible = position.bottom < windowHeight && position.bottom > 0;
  return topVisible || bottomVisible;
}

function imageLazyLoad() {
  const images = document.querySelectorAll('img')
  for (let img of images) {
    const realSrc = img.dataset.src
    if (!realSrc) continue
    if (isVisible(img)) {
      img.src = realSrc
      img.dataset.src = ''
    }
  }
}

// 测试
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))