金三银四面试题总结之手写算法

73 阅读4分钟

1.实现bind apply call

bind, apply, call的本质都是改变作用域. Function原型链中的this, 指的是function本身

bind会返回一个新的函数

Function.prototype.myBind = function(context) {
  const key = Symbol('fn');
  context[key] = this;
  return function (...args) {
    const res = context[key](...args);
    delete context[key];
    return res;
  }
}

const test = {
  name: "fy",
  showName: function (last) {
    console.log(this.name + " is " + last);
  },
};
test.showName.myBind({ name: "Mr.fy" })("handsome");

call语法的重点是参数是分开传入的(没有处理async)

Function.prototype.myCall = function (context, ...args) {
  const key = Symbol('fn');
  context[key] = this;
  const res = context[key](...args);
  delete context[key];
  return res;
}
function testapply(arg1, arg2, arg3) { console.log('name: ', this.name, arg1, arg2, arg3);  }
testapply.call({name: 'aaa'}, 111,222,333);
testapply.myCall({name: 'aaa'}, 111,222,333);

apply语法的重点是参数被作为列表传入

Function.prototype.myApply = function (context, args) {
  const key = Symbol('fn');
  context[key] = this;
  const res = context[key](...args);
  delete context[key];
  return res;
}
function testapply(arg1, arg2, arg3) { console.log('name: ', this.name, arg1, arg2, arg3);  }
testapply.call({name: 'aaa'}, [111,222,333]);
testapply.myCall({name: 'aaa'}, [111,222,333]);

2.new_create_instanceof

new操作符做了这些事:

  1. 创建一个全新的对象
    • 这个对象的__proto__要指向构造函数的原型prototype
    • 它将新生成的对象的 __proto__ 属性赋值为构造函数的 prototype 属性,使得通过构造函数创建的所有对象可以共享相同的原型。 这意味着同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的对象。
  2. 执行构造函数,使用 call/apply 改变 this 的指向
  3. 返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象
function myNew(constructor, ...args) {
// 创建一个新的空对象
  const obj = {};
  // 将这个空对象的__proto__指向构造函数的原型
  // obj.__proto__ = Con.prototype;
  Object.setPrototypeOf(obj, constructor.prototype);
  const res = constructor.apply(obj, args)
  return res instanceof Object ? res : obj;
}

实现object.create

function create(obj) {
  const newObj = {};
  newObj.__proto__ = obj;
  newObj.__proto__.constructor = obj.__proto__.constructor;
  return newObj;
}

实现instanceof instanceof的关键在于检查原型链

  • proto(隐式原型)与prototype(显式原型)
  • 每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数的原型对象, prototype是函数才有的属性
  • JavaScript中任意对象都有一个内置属性[[prototype]], 大多数浏览器都支持通过__proto__来访问
  • 隐式原型指向创建这个对象的函数(constructor)的prototype
function myInstanceOf(left, right) {
  const prototype = right.prototype;
  let proto = Object.getPrototypeOf(left);
  while(true) {
      if (!proto) return false;
      if (proto === prototype) return true;
      proto = Object.getPrototypeOf(proto);
  }
}

3.深拷贝

简单的深拷贝可以通过JSON.parse这种方式实现, 但这种方式无法解决function/Symbol/循环引用的情况.

function deepClone(obj) {
  const map = new WeakMap();
  function clone(target) {
    if (typeof target === 'object' && target !== null) {
      if (map.has(target)) {
        return map.get(target);
      }
      const result = Array.isArray(target) ? [] : {};
      map.set(target, result);
      for (let key in target) {
        if (target.hasOwnProperty(key)) {
          result[key] = clone(target[key]);
        }
      }
    } else {
      // 对于function, string, number, boolean, symbol, undefined, null, 都可以直接返回
      return target;
    }
  }
  return clone(obj);
}

4.节流防抖

防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数 如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算

function debounce(fn, wait) {
  let timeout = null;
  return function() {
    let context = this;
    let args = arguments;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  }
}

节流函数原理: 指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。

const throttle = (func, wait = 50) => {
  let lastTime = 0
  return function(...args) {
    let now = +new Date()
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

5.异步并发控制函数

异步并发控制器/调度器, 限制执行函数的同时最大执行数

  • 该函数返回一个执行函数, 该执行函数接收一个异步任务函数.
  • 执行函数被调用时, 会根据maxnum来执行task, 如果正在执行的task数不超过maxnum, 则立即执行, 否则会等到任意一个正在执行的task结束后执行, 并返回值为task返回值的Promise
function asyncTaskControler(capacity) {
  const waitting = [];
  let runningNum = 0;
  return async (func) => {
    if (runningNum >= capacity) {
      await new Promise(r => waitting.push(r));
    }
    runningNum++;
    try {
      return await func();
    } finally {
      runningNum--;
      const next = waitting.shift();
      next && next();  
    }
  }
}

另一种实现

const scheduler = (max) => {
  let num = 0;
  const list = [];
  return (task) => {
    return new Promise((resolve, reject) => {
      const exec = async () => {
        num++;
        task().then((res) => {
          resolve(res);
        }).catch((err) => {
          reject(err);
        }).finally(() => {
          num--;
          const next = list.shift();
          next && next();
        });
      }
      if (num < max) {
        exec();
      } else {
        list.push(exec);
      }
    })
  }
}

6.数组flat

数组flat方法是ES6新增的一个特性,可以将多维数组展平为低维数组。如果不传参默认展平一层,传参可以规定展平的层级。

function flat(array) {
  const res = [];
  for (const item of array) {
    if (Array.isArray(item)) {
      res.push(...flat(item));
    } else {
      res.push(item);
    }
  }
  return res;
}