前端常见手写题

161 阅读4分钟

前言:接上篇,本篇为面试中常见的手写题,最基础的东西一定要记牢,常见的实现方式,一些方法的实现原理,了然于心。

往期文章:

高频面试题整理

leetcode Hot 100 题目整理

1、手写 promiseAll、 promiseRace

function promiseAll(promiseArray) {
    return new Promise((resolve, reject) => {
      if(!Array.isArray(promiseArray)) {
        return reject(new Error('传入的参数必须是数组!'));
      }
      const res = [];
      const promiseNums = promiseArray.length;
      let counter = 0;
      for(let i = 0; i < promiseNums; i++) {
        //resolve直接返回处理后的Promise对象,不用再单独判断类型(promise或者常量)
        Promise.resolve(promiseArray[i]).then(value => {   
          counter++;   //push方法会导致返回的promise顺序错乱
          res[i] = value;
          if(counter === promiseNums) {
            resolve(res);
          }
        }).catch(e => reject(e));
      }
    })
  }

  function promiseRace(promiseArr) {
    return new Promise((resolve, reject) => {
      for (let i = 0; i < promiseArr.length; i++) {
        Promise.resolve(promiseArr[i]).then(
          (res) => {
            //promise数组只要有任何一个promise 状态变更  就可以返回
            resolve(res);
          },
          (err) => {
            reject(err);
          }
        );
      }
    });
  }

2、并发控制

function limitLoad(urls, handler, limit) {
    const sequence = [].concat(urls); // 先拷贝一下
    let promises = [];
    promises = sequence.splice(0, limit).map((url, index) => {
      return handler(url).then(() => {
        return index;  // 返回前Limit个中最快的元素索引
      })
    })
    let p = Promise.race(promises);
    for(let i = 0; i < sequence.length; i++) {
      p = p.then((res) => {  // 把剩下的元素依次推入
        promises[res] = handler(sequence[i]).then(() => {
          return res;
        })
        return Promise.race(promises);
      })
    }
  }

3、重写xhr

class XhrHook {
    constructor(beforeHooks = {}, afterHooks = {}) {
      this.XHR = window.XMLHttpRequest;
      this.beforeHooks = beforeHooks;
      this.afterHooks = afterHooks;
      this.init();
    }
    
    init() {
      let _this = this;
      window.XMLHttpRequest = function() { // 不能用箭头函数,new 时要有自己的this指向
        this._xhr = new _this.XHR();
        _this.overwrite(this);
      }
    }
    
    overwrite(proxyXHR) {
      for(let key in proxyXHR._xhr) {
        if(typeof proxyXHR._xhr[key] === 'function') {
          this.overwriteMethod(key, proxyXHR);
          continue;
        }
        this.overwriteAttributes(key, proxyXHR);
      }
    }
    //重写方法
    overwriteMethod(key, proxyXHR) {
      let beforeHooks = this.beforeHooks;  //我们应该可以拦截原有行为
      let afterHooks = this.afterHooks;
      proxyXHR[key] = (...args) => {
        //拦截
        if(beforeHooks[key]) {
          const result = beforeHooks[key].call(proxyXHR, args);
          if(result === false) return;
        }
        const res = proxyXHR._xhr[key].apply(proxyXHR._xhr, args);
        afterHooks[key] && afterHooks[key].call(proxyXHR._xhr, res);
        return res;
        
      }
    }
    //重写属性
    overwriteAttributes(key, proxyXHR) {
      Object.defineProperties(proxyXHR, key, this.setPropetyDescriptor(key, proxyXHR));
    }
    
    setPropetyDescriptor(key, proxyXHR) {
      let obj = Object.create(null); //  创建一个干净的空对象
      let _this = this;
      obj.set = function(val) {
        if(!key.startsWith('on')) {
          proxyXHR['__' + key] = val;
          return;
        }
        if(_this.beforeHooks[key]) {
          this._xhr[key] = function(...args) {
            _this.beforeHooks[key].call(proxyXHR);
            val.apply(proxyXHR, args);
          }
          return;
        }
        this._xhr[key] = val;
      }
      obj.get = function() {
        return proxyXHR['__' + key] || this._xhr[key];
      }
      return obj;
    }
  }
  
  var xhr = new XMLHttpRequset();

4、实现 sizeof

function sizeOfObject(object) {
    const seen = new WeakSet(); // 存已经计算过的内存:key和value (c:xxx  d:xxx)
  	if(object === null) return 0;
    let bytes = 0;
    //对象里的key也是占用内存空间的
    const properties = Object.keys(object);
    for(let i = 0; i < properties.length; i++) {
    	const key = properties[i];
      bytes += calculator(key); // 重复引用的key也要计算
      if(typeof object[key] === 'object' && object[key] !== null) { // typeof(null) 返回object
      	if(seen.has(object[key])) {
        	continue;
        }
        seen.add(object[key]);
      }
      bytes += calculator(object[key]);
    }
    return bytes;
  }
  
  function calculator(object) {
  	const objectType = typeof object;
    switch(objectType) {
    	case 'string': {
      	return object.length * 2;
      }
      case 'boolean': {
      	return 4;
      }
      case 'number': {
      	return 8;
      }
      case 'object': {
      	if(Array.isArray(object)) { // 数组嵌套的话需要递归
          return object.map(calculator).reduce((res, current) => res + current, 0);
        } else {
          return sizeOfObject(object);
        }
      }
      default: {
      	return 0;
      }  
    }
  }

5、发布订阅

class EventEmitter {
    constructor(maxListeners) {
      this.events = {};  //用来存储事件和监听函数之间的关系
      this.maxListeners = maxListeners || Infinity;
    }
    emit(event, ...args) {
      const cbs = this.events[event];
      if(!cbs) {
        console.log('没有这个事件');
        return this;
      }
      cbs.forEach(cb => cb.apply(this, args));
      return this; // 方便链式调用
    }
    on(event, cb) {
      if(!this.events[event]) {
        this.events[event] = [];
      }
      //拦截最大监听
      if(this.maxListeners !== Infinity && this.events[event].length >= this.maxListeners) {
        console.warn(`当前事件${event}超过最大监听数`);
        return this;
      }
      this.events[event].push(cb);
      return this;
    }
    once(event, cb) {
      const func = (...args) => {
        this.off(event, func);
        cb.apply(this, args);
      }
      this.on(event, func);
      return this;
    }
    off(event, cb) {
      if(!cb) {
        this.events[event] = null;
      } else {
        this.events[event] = this.events[event].filter(item => item !== cb);
      }
      return this;                                                 
    }
  }

6、实现 LazyMan

class LazyMan {
    constructor(name) {
        this.name = name;
        this.taskQueue = [];
        this.named(name);
    }

    named(name) {
        this.taskQueue.push(() =>  {
            console.log(`I am ${name}`);
            this.next();
        })
        return this;
    }

    sleep(delay) {
        this.taskQueue.push(() =>  {
            setTimeout(() => {
                console.log(`I am sleeping for ${delay} ms`);
                this.next();
            }, delay)
        })
        return this;
    }

    eat() {
        this.taskQueue.push(() => {
            console.log(`I am having dinner`);
            this.next();
        })
        return this;
    }

    start() {
        this.taskQueue.shift()();
    }

    next() {
        if(this.taskQueue.length) {
            const task = this.taskQueue.shift();
            task();
        }
    }
  }

  class LazyMan {
    constructor(name) {
        this.name = name;
        this.taskQueue = [];
        this.named(name);
        Promise.resolve()
            .then(res => this.start())
    }

    named(name) {
        this.taskQueue.push(() => new Promise((resolve, reject) => {
            console.log(`I am ${name}`);
            resolve();
        }))
    }

    sleep(delay) {
        this.taskQueue.push(() => new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(`I am sleeping for ${delay} ms`);
                resolve();
            }, delay)
        }))
        return this;
    }

    eat() {
        this.taskQueue.push(() => new Promise((resolve, reject) => {
            console.log(`I am having dinner`);
            resolve();
        }))
        return this;
    }

    start() {
        this.taskQueue.reduce((total, fn) => total.then(fn), Promise.resolve());
    }
  }

  const halk = new LazyMan('halk');
  halk.sleep(1000).eat().sleep(1000).eat();

7、防抖

const useDebounce = (fn, interval, args) => {
    useEffect(() => {
      const timer = setTimeout(fn.bind(null, args), interval);
      return () => {
        clearTimeout(timer);
      }
    }, args)
  }

  function debounce(fn, interval) {
    let timer = null;
    return function() {
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.apply(this, arguments);
      }, interval);
    }
  }

8、节流

// 首节流
  function throttle(fn, interval) {
    let last = 0;
    return function() {
      let now = Date.now();
      if(now - last >= interval) {
        last = now;
        fn.apply(this, arguments);
      }
    }
  }
  // 尾节流,不会立即执行函数,delay后执行,最后一次停止触发后,还会在执行一次
  function throttle1(fn, interval) {
    let timer = null;
    return function() {
      if(!timer) {
        timer = setTimeout(() => {
          fn.apply(this, arguments);
          timer = null;
        }, interval)
      }
    }
  }

  function throttle2(fn, interval) {
    let timer = null;
    let start = Date.now();
    return function() {
      let cur = Date.now();
      let remain = interval - (cur - start);
      clearTimeout(timer);
      if(remain <= 0) {
        fn.apply(this, arguments);
        start = Date.now();
      } else {
        timer = setTimeout(() => {
          fn.apply(this, arguments);
          start = Date.now();
        }, remain);
      }
    } 
  }

9、mockSetInterval

function mockSetInterval(fn, delay, ...args) {
    let timer = null;
    const recur = function() {
      timer = setTimeout(() => {
        fn.apply(this, args);
        recur();
      }, delay)
    }
    recur();
  }

  function mockClearInterval(id) {
    clearTimeout(id);
  }

10、对象响应式

  const reactive = obj => {
     if(typeof obj === 'object') {
       for(const key in obj) {
         defineReactive(obj, key, obj[key]);
       }  
     }
  }

  const defineReactive = (obj, key, val) => {
    reactive(val); // 递归
    Object.defineProperty(obj, key, {
      get() {
        return val;
      },
      set(newVal) {
        if(val === newVal) {
          return;
        }
        val = newVal;
        render(key, val);
      }
    })
  }

  const render = (key, val) => {
    console.log(`SET key=${key} val=${val}`);
  }

11、数组响应式

  const arrPrototype = Array.prototype; // 保存数组的原型
  const newArrProtoType = Object.create(arrPrototype); // 创建一个新的数组原型

  ['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(methodName => {
    newArrProtoType[methodName] = function() {
      arrPrototype[methodName].call(this, ...arguments); // 执行原有数组的方法
      render1(methodName, ...arguments);
    }
  })

  const render1 = (action, ...args) => {
    console.log(`Action = ${action} args=${args.join(',')}`)
  }

  const reactive1 = obj => {
    if(Array.isArray(obj)) {
      // 把新定义的原型对象指向 obj.__proto__
      obj.__proto__ = newArrProtoType;
    }
  }

12、基于 proxy 监听属性删除

  function makeObservable(target) {
    let observeStore = new Map();
    let handlerName = Symbol('handler');
    observeStore.set(handlerName, []);
    target.observe = function(handler) {
      observeStore.get(handlerName).push(handler);
    }
    const proxyHandler = {
      get(target, property, receiver) {
        if(typeof target[property] === 'object' && target[property] !== null) {
          return new Proxy(target[property], proxyHandler);
        }
        let success = Reflect.get(...arguments);
        if(success) {
          observeStore.get(handlerName).forEach(handler => handler('GET', property, target[property]));
        }
        return success;
      },
      set(target, property, value, receiver) {
        let success = Reflect.set(...arguments);
        if(success) {
          observeStore.get(handlerName).forEach(handler => handler('SET', property, value));
        }
        return success;
      },
      deleteProperty(target, property) {
        let success = Reflect.deleteProperty(...arguments);
        if(success) {
          observeStore.get(handlerName).forEach(handler => handler('DELETE', property));
        }
        return success;
      }
    };
    // 创建proxy,拦截更改
    return new Proxy(target, proxyHandler);
  }

  let user = {}

  user = makeObservable(user);

  user.observe((action, key, value) => {
    console.log(`${action} key=${key} value=${value || ''}`);
  })

13、将虚拟dom转为真实数据结构

  function render(vnode) {
    if(typeof vnode === 'number') {
      vnode = String(vnode);
    }
    if(typeof vnode === 'string') {
      return document.createTextNode(vnode);
    }
    const element = document.createElement(vnode.tag);
    if(vnode.attrs) {
      Object.keys(vnode.attrs).forEach(attrKey => {
        element.setAttribute(attrKey, vnode.attrs[attrKey]);
      })
    }
    if(vnode.children) {
      vnode.children.forEach(childNode => {
        element.appendChild(render(childNode));
      })
    }
    return element;
  }

14、实现对象可遍历

  const obj = {
    count: 0,
    [Symbol.iterator]: () => {
      return {
        next: () => {
          obj.count++;
          if(obj.count <= 10) {
            return {
              value: obj.count,
              done: false
            }
          } else {
            return {
              value: undefined,
              done: true
            }
          }
        }
      }
    }
  }

  for(const item of obj) {
    console.log(item);  // 1 2 3 4 5 6 7 8 9 10
  }

15、深拷贝

  function deepClone(obj, hash = new WeakMap()) {
    if(obj === null) return null;
    if(obj instanceof Date) return new Date(obj);
    if(obj instanceof RegExp) return new RegExp(obj);
    if(typeof obj !== 'object') return obj;
    if(hash.has(obj)) return hash.get(obj);
    const resObj = Array.isArray(obj) ? [] : {};
    hash.set(obj, resObj); // 弱引用,保存key,但不保存引用,不影响jc
    Reflect.ownKeys(obj).forEach(key => {
      resObj[key] = deepClone(obj[key], hash);
    })
    return resObj;
  }

  function instanceOf(left, right) {
    if(typeof left !== 'object' || left === null) {
      return false;
    }
    while(true) {
      if(left === null) return false;  
      if(left.__proto__ === right.prototype) return true;
      left = left.__proto__;
    }
  }

16、compose

function compose() {
    const argFnList = [...arguments];
    return num => {
      return argFnList.reduce((pre, cur) => cur(pre), num);
    }
  }

  const a = compose(fn1,fn2,fn3,fn4);
  a(3);

17、柯理化

function currying(fn, ...args) {
    const originFnArgumentLength = fn.length;
    let allArgs = [...args];
    const resFn = (...newArgs) => {
      allArgs = [...allArgs, ...newArgs];
      if(allArgs.length === originFnArgumentLength) {
        return fn(...allArgs);
      } else {
        return resFn;
      }
    }
    return resFn;
  }

  const add = (a, b, c) => a + b + c;
  const a1 = currying(add, 1);
  const a2 = a1(2);
  console.log(a2(3)); // 6

18、实现 map、reduce

function reduce(fn, initVal) {
    let pre = initVal ? initVal : 0;
    for(let i = 0; i < this.length; i++) {
        pre = fn(pre, this[i]);
    }
    return pre;
  }

  function map(fn) {
    let res = [];
    for(let i = 0; i < this.length; i++) {
        res.push(fn(this[i]));
    }
    return res;
  }

19、实现 new

function _new() {
    let obj = {};
    let [constructor, ...args] = [...arguments];
    obj.__proto__ = constructor.prototype;
    let result = constructor.apply(obj, args);
    if (result && typeof result === 'function' || typeof result === 'object') {
        return result;
    }
    return obj;
  }

20、实现 bind、call、apply

 function bind(context, ...args) {
    return (...newArgs) => {
        return this.call(context, ...args, ...newArgs);
    }
  }
  
  function call(context, ...args) {
      context = Object(context) || window;
      let fn = Symbol(1);
      context[fn] = this;
      let result = context[fn](...args);
      delete context[fn];
      return result;
  }
  
  function apply() {
      let [thisArg, args] = [...arguments];
      thisArg = Object(thisArg);
      let fn = Symbol();
      thisArg[fn] = this;
      let result = thisArg[fn](...args);
      delete thisArg[fn];
      return result;
  }

21、数组、对象扁平化

function flatdeep(arr) {
    return arr.reduce((res, cur) => {
      if(Array.isArray(cur)) {
        return [...res, ...flatdeep(cur)];
      } else {
        return [...res, cur];
      }
    }, [])
  }

  function flatten(obj, key = '', res = {}, isArray = false) {
    for (let [k, v] of Object.entries(obj)) {
      if(v) {
        if(Array.isArray(v)) {
          let temp = isArray ? `${key}[${k}]` : `${key}${k}`;
          flatten(v, temp, res, true);
        } else if(typeof v === 'object') {
          let temp = isArray ? `${key}[${k}].` : `${key}${k}.`;
          flatten(v, temp, res);
        } else {
          let temp = isArray ? `${key}[${k}]` : `${key}${k}`;
          res[temp] = v;
        }
      }
    }
    return res;
}

const input = {
    a: 1,
    b: [1, 2, { c: true }, [3]],
    d: { e: 2, f: 3 },
    g: null,
};

console.log(flatten(input));

22、下划线转驼峰

function transform(str) {
  const arr = str.split('_');
  return arr.reduce((pre, cur) => {
      if(pre === '') {
          return cur.slice(0, 1).toLowerCase() + cur.slice(1);
      } else {
          return pre + cur.slice(0, 1).toUpperCase() + cur.slice(1);
      }

  }, '');
}

23、匹配手机号

function phone(nums) {
  const reg = /^[1][3,4,5,7,8,9][0-9]{9}$/;
  if(reg.test(nums)) return true;
  return false;
}

24、数字变为数组

Array.from({length: 5}, (v, i) => i);
Array.from(Array(5), (v, i) => i);
// [0, 1, 2, 3, 4]

25、树转 Json

function convert(data, pid) {
  let list = data.filter(item => item.pid === pid);
  list.forEach(item => item.children = convert(data, item.id));
  return list;
}

function convert(data) {
  const map = {}, res = [];
  data.forEach(item => {
      if(!item.children) item.children = [];
      map[item.id] = item;
      if(map[item.pid]) {
          map[item.pid].children.push(item);
      } else {
          res.push(item);
      }
  })
  return res;
}

let arr = [
  {id: 1, name: '部门1', pid: 0},
  {id: 2, name: '部门2', pid: 1},
  {id: 3, name: '部门3', pid: 1},
  {id: 4, name: '部门4', pid: 3},
  {id: 5, name: '部门5', pid: 4},
]

26、LRU

var LRUCache = function(capacity) {
  this.capacity = capacity;
  this.map = new Map();
};

LRUCache.prototype.get = function(key) {
  if(this.map.has(key)) {
      let temp = this.map.get(key);
      this.map.delete(key);
      this.map.set(key, temp);
      return temp;
  } else {
      return -1;
  }
};

LRUCache.prototype.put = function(key, value) {
  if(this.map.has(key)) {
      this.map.delete(key);
  }
  this.map.set(key, value);
  if(this.map.size > this.capacity) {
      this.map.delete(this.map.keys().next().value);
  }
};