前端面试之手写代码

150 阅读4分钟

引子

本篇是面试中常见的手写代码集合。这些手写代码的题目,在一定程度上能体现候选人的代码功底和编程经验。熟练理解并写出这些题目,不仅可以应对手写代码本身,在回答相关的问题时,也能有更好的表现。

参数的获取和操作

参数的获取和操作有很多写法,我们新旧共赏,后面的代码我都会采取定义参数的方式

// 获取第一个参数
Array.prototype.slice(arguments, 0, 1);

// 获取除了第一个参数之外的参数
Array.prototype.slice(arguments, 1);

// 直接把 arguments 转化成数组再操作
Array.from(arguments)

// 定义函数参数
function (first, ...others) {}

数组去重

// ES6 新特性
function unique(arr) {
  return Array.from(new Set(arr));
}

// 利用 indexOf 返回第一个匹配索引
function unique(arr) {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}

数组乱序

// 为每个元素关联一个随机数,对随机数排序
function shuffle(arr) {
  return arr
    .map((item) => ({
      value: item,
      random: Math.random(),
    }))
    .sort((a, b) => a.random - b.random)
    .map((item) => item.value);
}

// 遍历,将当前元素与随机位置元素交换
function shuffle(arr) {
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    const j = Math.floor(Math.random() * len);
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
}

数组扁平化

// 递归
function flatten(arr) {
  return arr.reduce((prev, next) => {
    return prev.concat(Array.isArray(next) ? flatten(next) : next);
  }, []);
}

call、apply、bind

这三个函数方法很类似,参数都是 this 的指向和传递给原函数的参数。call 和 apply 只是入参形式不同,其他都一样;bind 返回新的函数。

Function.prototype._call = function (context, ...args) {
  // 顺便秀一下 Symbol 和 Reflect
  const fn = Symbol("fn");
  context[fn] = this;
  const result = context[fn](...args);
  Reflect.deleteProperty(context, fn);
  return result;
};

Function.prototype._apply = function (context, args) {
  context.fn = this;
  const result = context.fn(...args);
  delete context.fn;
  return result;
};

Function.prototype._bind = function (context, ...args) {
  const self = this;
  return function (...otherArgs) {
    const finalArgs = [...args, ...otherArgs];
    return self.apply(context, finalArgs);
  };
};

函数柯里化、函数组合

这两个方法能把函数式编程的精神体现出来,一个把提供参数的时机打散,一个把多个函数链式组合

function curry(fn) {
  return function judge(...args) {
    if (args.length === fn.length) {
      return fn(...args);
    } else {
      return function (...otherArgs) {
        return judge(...args, ...otherArgs);
      };
    }
  };
}

function compose(...fns) {
  return fns.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args))
  );
}

防抖、节流

防抖和节流都是限制调用频率的策略。防抖就像蓄力攻击,蓄力完成才能攻击,重新蓄力得重置时间;节流就像技能 CD,技能放完了得等 CD 才能再放。

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

function throttle(fn, wait) {
  let last = 0;
  return function () {
    const now = new Date();
    if (now - last > wait) {
      fn.apply(this, arguments);
      last = now;
    }
  };
}

原型链和继承

function _new(constructor, ...args) {
  const obj = new Object();
  obj.__proto__ = constructor.prototype;
  constructor.apply(obj, ...args);
  return obj;
}

function _instanceOf(obj, constructor) {
  let proto = obj.__proto__;
  while (proto) {
    if (proto === constructor.prototype) {
      return true;
    } else {
      proto = proto.__proto__;
    }
  }
  return false;
}

// 组合继承
function Father(firstname) {
  this.firstname = firstname;
}

function Son(firstname, secondname) {
  Father.call(this, firstname); // 1.call
  this.secondname = secondname;
}

Son.prototype = Object.create(Father.prototype); // 2.chain

深拷贝

function deepClone(obj, cache = new WeakMap()) {
  // 基本数据类型直接返回
  if (!isObject(obj)) {
    return obj;
  }

  // 缓存解决循环引用
  if (cache.get(obj)) return cache.get(obj);

  const type = getAbsoluteType(obj);

  // 高级对象类型特殊处理举例
  if (type === "date") return new Date(obj);
  if (type === "regexp") return new RegExp(obj);

  // {} []
  let cloneObj;
  if (type === "object") {
    cloneObj = {};
  } else if (type === "array") {
    cloneObj = [];
  } else {
    return `do not support ${type}`;
  }

  for (let key in obj) {
    const value = obj[key];
    cloneObj[key] = deepClone(value);
  }

  cache.set(obj, cloneObj);

  return cloneObj;
}

// 两个辅助函数也很有必要记一下
// 辅助函数1:判断是对象类型还是基本数据类型
function isObject(obj) {
  const type = typeof obj;
  return (type === "object" || type === "function") && obj !== null;
}

// 辅助函数2:获取精确类型
function getAbsoluteType(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}

JSON.stringify

// 思路和深拷贝类似,{}、[]、其他高级对象、各种基本数据类型
// 难点:字符串的拼接

function stringify(obj) {
  const type = getAbsoluteType(obj);

  // {} []
  const isObject = type === "object";
  const isArray = type === "array";

  if (isObject || isArray) {
    let json = [];
    for (let key in obj) {
      let val = obj[key];
      let strVal = stringify(val);
      json.push((isArray ? "" : `"${key}":`) + strVal);
    }
    return (isArray ? "[" : "{") + json + (isArray ? "]" : "}");
  }

  if (type === "string") return `"${obj}"`;

  // 尝试处理不同的对象类型
  if (type === "function") return "null";
  if (type === "date") return obj.toJSON();

  // 简单降级
  return String(obj);
}

function getAbsoluteType(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}

// 先把单元测试写出来是一个很好的方式
// 一边跑一边完善代码
const origin = [
  {
    string: "Jay",
    number: 40,
    bool: true,
    time: new Date(),
    null: null,
    undefined: undefined,
    func: () => {},
  },
  () => {},
];

console.log(stringify(origin));
console.log(JSON.stringify(origin));

Promise 与并发

完整版的 Promise 太长也太复杂,复杂的点在于如何实现链式调用,面试不太可能写完整版,但是你应该知道原理。另外 Promise 的几个并发方法也能考。

// 简单版 Promise
class MyPromise {
  constructor(executor) {
    this.status = "pendding";
    this.value = undefined;
    this.reason = undefined;

    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.status === "pendding") {
        this.status = "fulfilled";
        this.value = value;
        this.onResolvedCallbacks.forEach((fn) => fn(value));
      }
    };

    const reject = (reason) => {
      if (this.status === "pendding") {
        this.status = "rejected";
        this.reason = reason;
        this.onRejectedCallbacks.forEach((fn) => fn(reason));
      }
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    this.onResolvedCallbacks.push(onFulfilled);
    this.onRejectedCallbacks.push(onRejected);
  }
}

// 并发方法以最常考的 all 举例
// 另外三个 allSettled any race 写法思路都是一样的
function all(tasks) {
  return new Promise((resolve, reject) => {
    let resultArr = [];
    let count = 0;

    function collect(res, index) {
      resultArr[index] = res;
      count++;
      if (count === tasks.length) {
        resolve(resultArr);
      }
    }

    tasks.forEach((task, index) => {
      task.then((res) => collect(res, index), reject);
    });
  });
}

// 增加一点难度,allSettled + 限制最大并发数
function allSettled(tasks, max) {
  const len = tasks.length;

  let resultArr = new Array(len);
  let count = 0;

  // 开始时立即执行最大并发数量的任务
  while (count < max) {
    next();
  }

  function next() {
    const current = count++;

    // 判断是否还有剩余任务
    if (current >= len) {
      // 判断是否所有任务都已经结束
      if (!resultArr.includes(undefined)) {
        resolve(resultArr);
      }
      return;
    }

    const task = tasks[current];
    task.then(
      (res) => {
        resultArr[current] = res;
        // 每结束一个任务,执行下一个任务
        if (current < len) next();
      },
      (err) => {
        resultArr[current] = err;
        if (current < len) next();
      }
    );
  }
}

发布订阅

class EventBus {
  constructor() {
    this.callbackMap = {};
  }
  on(name, fn) {
    if (!this.callbackMap[name]) this.callbackMap[name] = [];
    this.callbackMap[name].push(fn);
  }
  off(name, fn) {
    const cbs = this.callbackMap[name] || [];
    this.callbackMap[name] = cbs.filter((cb) => cb !== fn);
  }
  once(name, fn) {
    const one = (...args) => {
      fn(...args);
      this.off(name, one);
    };
    this.on(name, one);
  }
  emit(name, ...args) {
    const cbs = this.callbackMap[name] || [];
    cbs.forEach((cb) => cb(...args));
  }
}