前端JS常见手写题总结

112 阅读11分钟

前端常见手写题总结,助力每一个面试的小伙伴。

需获取更良好的代码高亮阅读体验,请前往我的博客原文地址JS手写题大汇总 | 希亚的西红柿のBlog (refrain.cf)

JS 基础篇

深入了解 JS 这门语言的运行逻辑与机制,尝试实现其内部的方法。

ES5 实现继承

使用 ES5 的语法实现继承,注意静态方法的继承,好好体会下面两个例子。

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.say = () => {
  console.log("I'm a person!");
};
function Man(name, age, address) {
  Person.call(this, name, age);
  this.address = address;
}
Man.speak = () => {
  console.log("I'm a man!");
};
Object.setPrototypeOf(Man, Person);
Man.prototype = Object.create(Person.prototype);
Man.prototype.constructor = Man;
const person = new Person("person", 10);
const man = new Man("man", 11, "Tokyo");
Man.say();
Man.speak();
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  static say = () => {
    console.log("I'm a person!");
  };
}
class Man extends Person {
  constructor(name, age, address) {
    super(name, age);
    this.address = address;
  }
  static speak = () => {
    console.log("I'm a man!");
  };
}
const person = new Person("person", 10);
const man = new Man("man", 11, "Tokyo");
console.log(person, man);
console.log(Object.getPrototypeOf(Man) === Person);
Man.say();
Man.speak();

实现 new 函数

基本功无需多言,注意原型设置与执行结果的返回情况。

首先对问题进行分析,分析new关键字到底干了什么。

  1. 创建一个空对象将该对象的原型设置为函数的prototype
  2. 执行函数,this指向指向该对象。
  3. 若函数返回结果不为空且为对象,则返回该对象,否则返回先前生成的对象。
const myNew = (func, ...params) => {
  const newObj = Object.create(func.prototype);
  const result = func.apply(newObj, params);
  return typeof result === "object" && result !== null ? result : newObj;
};

手写 instanceof

纯粹的基本功,考察对原型链的理解深度。

// 手写一个 instanceof 函数
const myInstanceOf = (left, right) => {
  if (typeof left !== "object" || left === null) return false;
  const proto = right.prototype;
  let temp = Object.getPrototypeOf(left);
  while (temp) {
    if (temp === proto) return true;
    temp = Object.getPrototypeOf(temp);
  }
  return false;
};
const result = myInstanceOf(null, {});
console.log(result);

手写数据类型判断函数

Object.prototype.toString调用与正则,结合代码理解

const getTypeOf = (target) => {
  return Object.prototype.toString
    .call(target)
    .match(/^\[object (\w+)\]$/)[1]
    .toLowerCase();
};

手写 promisify 函数

注意promisify需要返回一个函数,这个函数返回一个promise对象。 注意剩余参数与展开语法的异同

/**
 * @param {(...args) => void} func
 * @returns {(...args) => Promise<any}
 */
const promisify = (func) => {
  return function (...params) {
    return new Promise((resolve, reject) => {
      func.call(this, ...params, (err, data) => {
        if (err) reject(err);
        else resolve(data);
      });
    });
  };
};

手写 async 方法

注意next的位置,以及两个return语句的使用。

const asyncFunc = () => {
  spawn(function* () {});
};
const spawn = (genF) => {
  return new Promise((resolve, reject) => {
    const gen = genF();
    const step = (nextF) => {
      let next;
      try {
        next = nextF();
      } catch (e) {
        return reject(e);
      }
      if (next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(
        (v) => step(() => gen.next(v)),
        (r) => step(() => gen.throw(r))
      );
    };
    step(() => gen.next(undefined));
  });
};

手写 EventEmitter 函数

  • 注意emit回调函数的this指向以及参数的问题。
  • 注意once函数可以采用闭包变量执行一次,或者采用闭包变量储存函数名,执行后移除。
class EventEmitter {
  constructor() {
    this.callbacks = {};
  }
  addListener(type, callback) {
    if (!this.callbacks[type]) {
      this.callbacks[type] = [];
    }
    this.callbacks[type].push(callback);
  }
  prependListener(type, callback) {
    if (!this.callbacks[type]) {
      this.callbacks[type] = [];
    }
    this.callbacks[type].unshift(callback);
  }
  on(type, callback) {
    this.addListener(type, callback);
  }
  removeListener(type, callback) {
    const index = this.callbacks[type].findIndex((item) => item === callback);
    this.callbacks[type].splice(index, 1);
  }
  off(type, callback) {
    this.removeListener(type, callback);
  }
  emit(type, ...args) {
    if (this.callbacks[type]) {
      this.callbacks[type].forEach((callback) => callback.apply(this, args));
    }
  }
  once(type, callback) {
    const _onceWrap = (func) => {
      const wrapFunc = function (...params) {
        func.apply(this, params);
        this.removeListener(type, wrapFunc);
      };
      return wrapFunc;
    };
    this.addListener(type, _onceWrap(callback));
  }
}
const ee = new EventEmitter();

// 注册所有事件
ee.once("wakeUp", (name) => {
  console.log(`${name} 1`);
});
ee.on("eat", (name) => {
  console.log(`${name} 2`);
});
ee.on("eat", (name) => {
  console.log(`${name} 3`);
});
const meetingFn = (name) => {
  console.log(`${name} 4`);
};
ee.on("work", meetingFn);
ee.on("work", (name) => {
  console.log(`${name} 5`);
});
ee.emit("wakeUp", "xx");
ee.emit("wakeUp", "xx"); // 第二次没有触发
ee.emit("eat", "xx");
ee.emit("work", "xx");
ee.off("work", meetingFn); // 移除事件
ee.emit("work", "xx"); // 再次工作

手写 call,apply,bind

super关键字可以作为构造函数在构造函数里面调用super关键字还做作为对象调用父对象上的方法或者静态方法,但在普通方法里面只能调用普通方法而不能调用静态方法,但在子类的静态方法中则可以都可以调用。

注意自己实现需要实现利用对象的方法隐式绑定做到的,注意bind关键字作为new方法调用的时候对this指向做出特殊的。

Function.prototype.myCall = function (obj, ...params) {
  const key = Symbol("key");
  obj[key] = this;
  const result = obj[key](...params);
  delete obj[key];
  return result;
};
Function.prototype.myApply = function (obj, params) {
  const key = Symbol("key");
  obj[key] = this;
  const result = obj[key](...params);
  delete obj[key];
  return result;
};
Function.prototype.myBind = function (obj, ...params) {
  const func = this;
  const bound = function (...args) {
    const context = Boolean(new.target) ? this : obj;
    func.myApply(context, [...params, ...args]);
  };
  bound.prototype = Object.create(func.prototype);
  bound.prototype.constructor = bound;
  return bound;
};

function say(arg1, arg2) {
  console.log(this.age, arg1, arg2);
}
let person = {
  age: 3,
};

let bindSay = say.myBind(person, "我叫", "nova");
bindSay();
new bindSay();

手写 Object.create

利用new关键字

function create(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

手写数组 push

const push = (arr, ...params) => {
  for (let i = 0; i < params.length; i++) {
    arr[arr.length] = params[i];
  }
  return arr.length;
};
const a = [1, 2, 3];
const b = push(a, 1, 2, 3);
console.log(a, b);

手写字符串 repeat

实现字符串的repeat方法,注意递归的核心为保证字符串不变

const repeat = (str, n) => {
  return new Array(n + 1).join(str);
};
// 核心为保证 str 不变
const repeat0 = (str, n) => {
  return n > 1 ? str.concat(repeat0(str, n - 1)) : str;
};
const a = repeat("sasda", 3);
console.log(a);

手写 Object.assign

注意对参数为剩余参数,同时Object强制转换对象会返回对象本身。

const objectAssign = (obj, ...sources) => {
  if (!obj) {
    throw new TypeError(
      "TypeError: Cannot convert undefined or null to object"
    );
  }
  const ret = Object(obj);
  sources.forEach((source) => {
    if (source)
      Object.keys(source).forEach((key) => {
        ret[key] = source[key];
      });
  });
  return ret;
};
const a = { a: 1 };
const b = { b: 2 };
const c = objectAssign(a, b);
console.log(a, b, c);

深入 JS 的使用

柯里化

经典无需多言

/**
 * @param { (...args: any[]) => any } fn
 * @returns { (...args: any[]) => any }
 */
const curry = (fn) => {
  return function curryInner(...params) {
    if (params.length < fn.length)
      return function (...args) {
        return curryInner(...params, ...args);
      };
    else return fn.apply(this, params);
  };
};

/**
 * @param { (...args: any[]) => any } fn
 * @returns { (...args: any[]) => any }
 */
const curry = (fn) => {
  return function curryInner(...params) {
    if (params.length < fn.length || params.includes(curry.placeholder)) {
      return function (...args) {
        params = params.map((item) => {
          if (item === curry.placeholder) {
            return args.shift();
          } else {
            return item;
          }
        });
        return curryInner(...params, ...args);
      };
    } else {
      return fn.apply(this, params);
    }
  };
};
curry.placeholder = Symbol();

深拷贝

Descriptor来全给你拷了。

  • 采用ES6之后的方法进行,原型链与属性修饰符

    const copy = (obj) => {
      const newObj = Object.create(
        Object.getPrototypeOf(obj),
        Object.getOwnPropertyDescriptors(obj)
      );
    };
    
  • 采用for循环实现深拷贝

    const cloneDeep = (data) => {
      // your code here
      const map = new WeakMap();
      const clone = (obj) => {
        if (typeof obj !== "object" || obj === null) return obj;
        if (map.has(obj)) return map.get(obj);
        const newObj = new obj.constructor();
        map.set(obj, newObj);
        Reflect.ownKeys(obj).forEach((key) => {
          newObj[key] = clone(obj[key]);
        });
        return newObj;
      };
      return clone(data);
    };
    

防抖与节流

简单复杂两种全部实现,结合代码理解,无需多言。

// 防抖
const debounce = (func, wait) => {
  let timer = null;
  return function (...params) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, params);
      timer = null;
    }, wait);
  };
};

const debounce = (func, wait, options = { leading: false, trailing: true }) => {
  let timer = null;
  return function (...params) {
    let isInvoked = false;
    if (timer) clearTimeout(timer);
    else if (options.leading) {
      func.apply(this, params);
      isInvoked = true;
    }
    timer = setTimeout(() => {
      if (options.trailing && !isInvoked) {
        func.apply(this, params);
      }
      timer = null;
    }, wait);
  };
};
// 节流
const throttle = (func, wait) => {
  let timer = null;
  let lastArgs = null;
  const run = () => {
    timer = null;
    if (lastArgs) {
      func.apply(this, lastArgs);
      lastArgs = null;
      timer = setTimeout(run, wait);
    }
  };
  return function (...params) {
    if (!timer) {
      func.apply(this, params);
      timer = setTimeout(run, wait);
    } else {
      lastArgs = params;
    }
  };
};

/**
 * @param {(...args: any[]) => any} func
 * @param {number} wait
 * @param {boolean} option.leading
 * @param {boolean} option.trailing
 * @returns {(...args: any[]) => any}
 */
const throttle = (func, wait, option = { leading: true, trailing: true }) => {
  let timer = null;
  let lastArgs = null;
  const run = () => {
    if (lastArgs && option.trailing) {
      func.apply(this, lastArgs);
      lastArgs = null;
      timer = setTimeout(run, wait);
    } else timer = null;
  };
  return function (...params) {
    if (!timer) {
      if (option.leading) {
        func.apply(this, params);
      }
      timer = setTimeout(run, wait);
    } else if (option.trailing) {
      lastArgs = params;
    }
  };
};

实现数组的 flat 方法

核心思路为递归,使用数组的reduce方法能减少不少代码量。

function flat(arr, depth = 1) {
  const results = [];
  const run = (items, count = 0) => {
    if (count > depth) return results.push(items);
    for (const item of items) {
      if (!Array.isArray(item)) {
        results.push(item);
      } else {
        run(item, count + 1);
      }
    }
  };
  run(arr);
  return results;
}
// 简化版,运行了数组的 reduce方法
function flat(arr, depth = 1) {
  return depth > 0
    ? arr.reduce(
        (pre, cur) =>
          pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : [cur]),
        []
      )
    : arr;
}

并发控制

  • 并发池

    async function asyncPool(poolLimit, array, iteratorFn) {
      const ret = [];
      const executing = [];
      for (const item of array) {
        const p = Promise.resolve(iteratorFn(item));
        ret.push(p);
        if (poolLimit < array.length) {
          const e = p.finally(() => executing.splice(executing.indexOf(e), 1));
          executing.push(e);
          if (executing.length >= poolLimit) {
            await Promise.race(executing);
          }
        }
      }
      return Promise.all(ret);
    }
    
  • 递归版本

    /**
     * @param {() => Promise<any>} func
     * @param {number} max
     * @return {Promise}
     */
    // 注意 resolve 的时机以及位置
    function throttlePromises(funcs, max) {
      return new Promise((resolve, reject) => {
        const queue = [...funcs];
        const results = [];
        let count = 0;
        const run = () => {
          while (queue.length && count < max) {
            const p = queue.shift()();
            count++;
            p.then(
              (v) => {
                results.push(v);
                count--;
                run();
              },
              (r) => {
                reject(r);
              }
            );
          }
          if (funcs.length === results.length) {
            resolve(results);
          }
        };
        run();.
      });
    }
    
  • 调度器版本

    注意add方法的返回值需要是一个promise对象,并且这个promise对象的状态应该由传入函数来决定。自动调度的核心机制还是递归。

    class Scheduler {
      constructor(size = 2) {
        this.isRunning = 0;
        this.queue = [];
        this.size = size;
      }
      add(promiseCreator) {
        return new Promise((resolve, reject) => {
          this.queue.push(() => promiseCreator().then(resolve, reject));
          this.executeNext();
        });
      }
      executeNext() {
        if (this.queue.length && this.isRunning < this.size) {
          this.isRunning++;
          const p = this.queue.shift();
          p().finally(() => {
            this.isRunning--;
            this.executeNext();
          });
        }
      }
    }
    
    const timeout = (time) =>
      new Promise((resolve) => {
        setTimeout(resolve, time);
      });
    const timeout0 = (time) =>
      new Promise((resolve, reject) => {
        setTimeout(reject, time);
      }).catch((e) => e);
    
    const scheduler = new Scheduler();
    const addTask = (time, order) => {
      scheduler.add(() => timeout(time)).then(() => console.log(order));
    };
    const addTask0 = (time, order) => {
      scheduler.add(() => timeout0(time)).then(() => console.log(order));
    };
    
    addTask(1000, "1");
    addTask0(500, "2");
    addTask(300, "3");
    addTask(400, "4");
    
    // output: 2 3 1 4
    // 一开始,1、2两个任务进入队列
    // 500ms时,2完成,输出2,任务3进队
    // 800ms时,3完成,输出3,任务4进队
    // 1000ms时,1完成,输出1
    // 1200ms时,4完成,输出4
    

实现 sleep 函数

利用迭代器来使asyncawait来阻塞当前函数的执行线程。

const sleep = (delay) =>
  new Promise((resolve) => {
    setTimeout(resolve, delay);
  });

async function test() {
  console.log(1);
  await sleep(1000);
  console.log("Stop for 1s!");
  await sleep(2000);
  console.log("Stop for 2s!");
  await sleep(3000);
  console.log("Stop for 3s!");
}
test();

业务场景题

  1. 使用promise封装一个ajax请求

    <body>
      <button>发送ajax请求</button>
      <script>
        //1.获取DOM元素对象
        let btn = document.querySelector("button");
        //2.绑定事件
        btn.onclick = function () {
          //3.创建promise实例对象
          const p = new Promise((resolve, reject) => {
            //4.创建ajax实例对象
            const xhr = new XMLHttpRequest();
            //5.打开请求
            xhr.open(
              "get",
              "https://www.yiketianqi.com/free/day?appid=82294778&appsecret=4PKVFula&unescape=1"
            );
            //6.发送请求
            xhr.send();
            //7.利用onreadystatechange事件
            xhr.onreadystatechange = function () {
              //8.判断
              if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                  resolve(xhr.responseText);
                } else {
                  reject(xhr.response);
                }
              }
            };
          });
          p.then(
            (value) => {
              console.log(JSON.parse(value));
            },
            (reason) => {
              console.log("获取信息失败");
            }
          );
        };
      </script>
    </body>
    
  2. 实现日期格式化函数

    const dateFormat = (dateString, format) => {
      const date = new Date(dateString);
      const day = date.getDay();
      const month = date.getMonth() + 1;
      const year = date.getFullYear();
      format = format.replace(/yyyy/, year);
      format = format.replace(/MM/, month);
      format = format.replace(/dd/, day);
      return format;
    };
    dateFormat("2020-12-01", "yyyy/MM/dd"); // 2020/12/01
    dateFormat("2020-04-01", "yyyy/MM/dd"); // 2020/04/01
    dateFormat("2020-04-01", "yyyy年MM月dd日"); // 2020年04月01日
    
  3. 交换a, b的值不能用临时变量

    b = a + b;
    a = b - a;
    b = b - a;
    
  4. 注意ajax一定需要send方法

    function getJSON(url) {
      const p = new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.responseType = "json";
        xhr.setRequestHeader("Accept", "application/json");
        xhr.onreadystatechange = () => {
          if (xhr.readyState === XMLHttpRequest.DONE) {
            if (xhr.status === 200) {
              return resolve(xhr.responseText);
            }
          }
          reject(xhr.statusText);
        };
        xhr.send();
      });
      return p;
    }
    
  5. 对数字千分位的格式化。

    /**
     * @param {number} num
     * @return {string}
     */
    
    // naive solution
    function addComma(num) {
      const sign = num < 0 ? -1 : 1;
      if (sign < 0) {
        num *= -1;
      }
    
      const str = num.toString();
      const [integer, fraction] = str.split(".");
    
      const arr = [];
    
      const digits = [...integer];
      for (let i = 0; i < digits.length; i++) {
        arr.push(digits[i]);
        // add extra commas
        // care for the 0
        const countOfRest = digits.length - (i + 1);
        if (countOfRest % 3 === 0 && countOfRest !== 0) {
          arr.push(",");
        }
      }
    
      const newInteger = (sign < 0 ? "-" : "") + arr.join("");
    
      if (fraction === undefined) return newInteger;
      return newInteger + "." + fraction;
    }
    
    // regular expression  1
    function addComma(num) {
      const str = num.toString();
      let [integer, fraction] = str.split(".");
    
      while (true) {
        const next = integer.replace(/(\d+)(\d{3})/, "$1,$2");
        // 12345,678
        // 12,345,678
        if (next === integer) {
          break;
        }
        integer = next;
      }
    
      if (fraction === undefined) return integer;
      return integer + "." + fraction;
    }
    
    // regular expressiong global,
    // (?= )
    function addComma(num) {
      const str = num.toString();
      let [integer, fraction] = str.split(".");
    
      integer = integer.replace(/(\d)(?=(\d{3})+$)/g, "$1,");
    
      if (fraction === undefined) return integer;
      return integer + "." + fraction;
    }
    
    const addComma = (num) => {
      let [former, latter] = String(num).split(".");
      const regex = /(\d)(?=(\d{3})+$)/g;
      former = former.replace(regex, "$1,");
      if (!latter) return former;
      return former + "." + latter;
    };
    
  6. 数组的乱序输出

    const a = [1, 2, 3, 4, 5];
    const randomOutput = (arr) => {
      for (let i = 0; i < arr.length; i++) {
        const index = Math.floor(Math.random() * (arr.length - i)) + i;
        [arr[index], arr[i]] = [arr[i], arr[index]];
        console.log(arr[i]);
      }
    };
    const randomOutput0 = (arr) => {
      let length = arr.length;
      while (length) {
        const index = Math.floor(Math.random() * length--);
        [arr[index], arr[length]] = [arr[length], arr[index]];
        console.log(arr[length]);
      }
    };
    randomOutput(a);
    console.log(a);
    
  7. 实现数组的斜向打印

    function printMatrix(arr) {
      const m = arr.length,
        n = arr[0].length;
      const result = [];
      for (let i = 0; i < n; i++) {
        for (let j = 0, k = i; k >= 0 && j < m; j++, k--) {
          result.push(arr[j][k]);
        }
      }
      for (let i = 1; i < m; i++) {
        for (let j = n - 1, k = i; j > 0 && k < m; k++, j--) {
          result.push(arr[k][j]);
        }
      }
      return result;
    }
    console.log(
      printMatrix([
        [1, 2, 3, 4],
        [4, 5, 6, 4],
        [7, 8, 9, 4],
      ])
    );
    
  8. 电话号码的打印:

    const changeNum = (str) => {
      return str.replace(/(\d{3})(\d{4})(\d{4})/, "$1****$2");
    };
    const changeNum0 = (str) => {
      const arr = Array.from(str);
      arr.splice(3, 4, "****");
      return arr.join("");
    };
    const changeNum1 = (str) => {
      return str.replace(str.slice(3, 7), "****");
    };
    
    console.log(changeNum1("15727709770"));
    
  9. 循环打印方案

    const task = (light, delay, callback) => {
      setTimeout(() => {
        if (light === "red") {
          console.log("yellow");
        } else if (light === "yellow") {
          console.log("green");
        } else if (light === "green") {
          console.log("red");
        }
        callback();
      }, delay);
    };
    const step = () => {
      task("green", 1000, () =>
        task("red", 1000, () => task("yellow", 1000, () => step()))
      );
    };
    step();
    // 利用 promise 包装对象链式递归调用
    const task = (delay, light) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (light === "red") {
            console.log("red");
          } else if (light === "yellow") {
            console.log("yellow");
          } else if (light === "green") {
            console.log("green");
          }
          resolve();
        }, delay);
      });
    };
    
    const step = () => {
      const p = new Promise((resolve, reject) => {
        resolve(task(1000, "red"));
      });
      p.then(() => task(1000, "green"))
        .then(() => task(1000, "yellow"))
        .then(step);
    };
    step();
    
    const taskMaker = () => {
      let run = true;
      const stop = () => (run = false);
      const execute = async () => {
        while (run) {
          await task(1000, "red");
          await task(1000, "yellow");
          await task(1000, "green");
        }
      };
      return { stop, execute };
    };
    const { stop, execute } = taskMaker();
    execute();
    console.time("Test");
    setTimeout(() => {
      console.log("stop");
      stop();
      console.timeEnd("Test");
    }, 4000);
    
  10. 丢手帕问题

    const Josephu = (num, count) => {
      const circle = [];
      for (let i = 0; i < num; i++) {
        circle[i] = i + 1;
      }
      let counter = 0;
      let out = 0;
      for (let i = 0; i < circle.length; i++) {
        if (out >= circle.length - 1) break;
        if (circle[i]) {
          counter++;
          if (counter === count) {
            circle[i] = 0;
            counter = 0;
            out++;
          }
        }
        if (i === circle.length - 1) i = -1;
      }
      for (let i = 0; i < circle.length; i++) {
        if (circle[i]) return circle[i];
      }
    };
    function childNum(num, count) {
      let allplayer = [];
      for (let i = 0; i < num; i++) {
        allplayer[i] = i + 1;
      }
    
      let exitCount = 0; // 离开人数
      let counter = 0; // 记录报数
      let curIndex = 0; // 当前下标
    
      while (exitCount < num - 1) {
        if (allplayer[curIndex] !== 0) counter++;
    
        if (counter == count) {
          allplayer[curIndex] = 0;
          counter = 0;
          exitCount++;
        }
        curIndex++;
        if (curIndex == num) {
          curIndex = 0;
        }
      }
      for (i = 0; i < num; i++) {
        if (allplayer[i] !== 0) {
          return allplayer[i];
        }
      }
    }
    console.log(Josephu(39, 6));
    console.log(childNum(39, 6));
    
  11. 查找文章中出现频率最高的单词

    const findMostWord = (article) => {
      if (!article) {
        console.error("Argument cannot be undefined or null!");
      }
      const str = article.trim().toLowerCase();
      const words = str.match(/[a-z]+/g);
      let maxCount = 0,
        maxStr = "",
        set = new Set();
      words.forEach((item) => {
        if (!set.has(item)) {
          set.add(item);
          const count = str.match(new RegExp(`\\b${item}\\b`, "g")).length;
          if (count > maxCount) {
            maxStr = item;
            maxCount = count;
          }
        }
      });
      return maxStr + ":" + maxCount;
    };
    const findMostWord0 = (article) => {
      // 合法性判断
      if (!article) return;
      // 参数处理
      article = article.trim().toLowerCase();
      let wordList = article.match(/[a-z]+/g),
        visited = [],
        maxNum = 0,
        maxWord = "";
      article = " " + wordList.join("  ") + " ";
      // 遍历判断单词出现次数
      wordList.forEach(function (item) {
        if (visited.indexOf(item) < 0) {
          // 加入 visited
          visited.push(item);
          let word = new RegExp(" " + item + " ", "g"),
            num = article.match(word).length;
          if (num > maxNum) {
            maxNum = num;
            maxWord = item;
          }
        }
      });
      return maxWord + "  " + maxNum;
    };
    console.log(findMostWord0("a a a a a a bbb bbb bbb b b b b b b b b b b b"));
    
  12. setTimeout模仿setInterval

    const mySetInterval = (fn, wait) => {
      const timer = { flag: true };
      const step = () => {
        if (timer.flag) {
          fn();
          setTimeout(step, wait);
        }
      };
      step();
      return timer;
    };
    const timer = mySetInterval(() => console.log(10), 1000);
    setTimeout(() => (timer.flag = false), 5000);
    
  13. 判断对象中是否存在循环引用

    const isCircle = (target) => {
      const set = new Set();
      const step = (obj) => {
        if (typeof obj !== "object" || obj === null) return false;
        if (set.has(obj)) return true;
        set.add(obj);
        for (const key of Reflect.ownKeys(obj)) {
          const result = step(obj[key]);
          if (result) return true;
        }
      };
      return step(target);
    };
    const a = { a: 1 };
    a.b = a;
    console.log(a);
    console.log(isCircle(a.b));
    
  14. 手写一个nullToUndefined函数

    /**
     * @param {any} arg
     * @returns any
     */
    const undefinedToNull = (arg) => {
      if (arg === undefined) return null;
      if (typeof arg !== "object" || arg === null) return arg;
      for (const key in arg) {
        arg[key] = undefinedToNull(arg[key]);
      }
      return arg;
    };
    
  15. 判断字符串的有效数字

    /**
     * @param {string} str
     * @returns {boolean}
     */
    const validateNumberString = (str) => {
      return str !== "" && !isNaN(str);
    };
    const validateNumberString = (str) => {
      return /^[+-]?(\d+(\.\d*)?|\d*\.\d+)(e[+-]?\d+)?$/i.test(str);
    };
    
  16. 实现一个累加器

    /**
     * @param {number} num
     */
    const sum = (count) => {
      function sumInner(number) {
        return sum(count + number);
      }
      sumInner.valueOf = () => count;
      return sumInner;
    };
    
  17. nullToUndefined简单递归

    /**
     * @param {any} arg
     * @returns any
     */
    const undefinedToNull = (arg) => {
      if (arg === undefined) return null;
      if (typeof arg !== "object" || arg === null) return arg;
      for (const key in arg) {
        arg[key] = undefinedToNull(arg[key]);
      }
      return arg;
    };
    
  18. counter function自执行包裹

    const count = (() => {
      let num = 0;
      function func() {
        return ++num;
      }
      func.reset = () => {
        num = 0;
      };
      return func;
    })();
    
  19. counter对象,简单的数据代理

    /**
     * @returns { {count: number}}
     */
    const createCounter = () => {
      let count = 0;
      return Object.defineProperty({}, "count", {
        get() {
          return count++;
        },
        set() {
          console.log("it cannot be altered");
        },
      });
    };
    
  20. 失败后自动发起请求,超时后停止。

    /**
     * @param {() => Promise<any>} fetcher
     * @param {number} maximumRetryCount
     * @return {Promise<any>}
     */
    function fetchWithAutoRetry(fetcher, maximumRetryCount) {
      // your code here
      return new Promise((resolve, reject) => {
        let count = 0;
        const run = () => {
          fetcher().then(resolve, (r) => {
            console.log(r);
            count++;
            if (count > maximumRetryCount) return reject(r);
            run();
          });
        };
        run();
      });
    }
    
  21. 实现斐波那契数列的两种方式。

    const fib = (n, a = 0, b = 1) => {
      if (n < 1) return a;
      return fib(n - 1, b, a + b);
    };
    
    const fib = (n) => {
      if (n === 0) return 0;
      if (n === 1) return 1;
      return fib(n - 1) + fib(n - 2);
    };
    
    fib(10); // 55
    fib(1000); // timeout
    
  22. 创建一个LazyMan函数

    // interface Laziness {
    //   sleep: (time: number) => Laziness
    //   sleepFirst: (time: number) => Laziness
    //   eat: (food: string) => Laziness
    // }
    
    /**
     * @param {string} name
     * @param {(log: string) => void} logFn
     * @returns {Laziness}
     */
    const LazyMan = (name, logFn) => {
      const tasks = [() => logFn(`Hi, I'm ${name}.`)];
      const eat = (food) => logFn(`Eat ${food}.`);
      const sleep = (time) =>
        new Promise((resolve) => setTimeout(() => resolve(), 1000 * time)).then(
          () => logFn(`Wake up after ${time} second${time > 1 ? "s" : ""}.`)
        );
      setTimeout(async () => {
        for (const func of tasks) {
          await func();
        }
      });
      return {
        eat(food) {
          tasks.push(() => eat(food));
          return this;
        },
        sleep(time) {
          tasks.push(() => sleep(time));
          return this;
        },
        sleepFirst(time) {
          tasks.unshift(() => sleep(time));
          return this;
        },
      };
    };
    
  23. JSON文件转化为树结构的问题,关键在于利用对象的引用和map结构来进行操作。

    面试的时候这么简单的题居然没做出来,顶级 🤡,DFS入脑了看啥都想DFS,其实只要对每个对象建立一个Map,然后遍历对象进行赋值就好了,主要还是利用了对象本身的唯一性。

    const 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 },
    ];
    const target = [
      {
        id: 1,
        name: "部门1",
        pid: 0,
        children: [
          {
            id: 2,
            name: "部门2",
            pid: 1,
            children: [],
          },
          {
            id: 3,
            name: "部门3",
            pid: 1,
            children: [],
          },
        ],
      },
    ];
    const convertToTree = (arr) => {
      const map = new Map();
      arr.forEach((item) => {
        map.set(item.id, item);
      });
      arr.forEach((item) => {
        if (map.has(item.pid)) {
          const parent = map.get(item.pid);
          if (parent.children || (parent.children = [])) {
            parent.children.push(item);
          }
        }
      });
      return [arr[0]];
    };
    console.log(convertToTree(arr));
    
  24. 封装一个fetch请求

    class HTTPRequestUtil {
      async get(url) {
        const res = await fetch(url);
        const data = await res.json();
        return data;
      }
      async post(url, data) {
        const res = await fetch(url, {
          method: "POST",
          headers: {
            "Content-type": "application/json",
          },
          body: JSON.stringify(data),
        });
        const result = await res.json();
        return result;
      }
      async put(url, data) {
        const res = await fetch(url, {
          method: "PUT",
          headers: {
            "Content-type": "application/json",
          },
          body: JSON.stringify(data),
        });
        const result = await res.json();
        return result;
      }
      async delete(url, data) {
        const res = await fetch(url, {
          method: "DELETE",
          headers: {
            "Content-type": "application/json",
          },
          body: JSON.stringify(data),
        });
        const result = await res.json();
        return result;
      }
    }