本文主要记录了关于前端面试常考的手写代码题,常看常复习。

JS 基础篇

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

ES5 实现继承

使用 ES5 的语法实现继承,有几个注意点:

  • 使用Object.create方法,构造以父类的prototype为原型的新对象,防止对子类原型对象的修改影响到父类的原型对象。
  • 注意静态方法的继承,使用Object.setPrototypeOf将父类设置为子类的原型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();

验证的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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. 若函数返回结果不为空且为对象,则返回该对象,否则返回先前生成的对象。
1
2
3
4
5
6
7
8
9
10
/**
 * @param {Function} constructor
 * @param {any[]} args - argument passed to the constructor
 * `myNew(constructor, ...args)` should return the same as `new constructor(...args)`
 */
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

沿着原型链循环查找,即寻找当前对象实例的__proto____proto__,是否等于构造函数的prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * @param {any} obj
 * @param {target} target
 * @return {boolean}
 */
const myInstanceOf = (obj, target) => {
  if (typeof obj !== "object" || obj === null) {
    return false;
  }
  const proto = target.prototype;
  let curProto = Object.getPrototypeOf(obj);
  while (curProto) {
    if (curProto === proto) {
      return true;
    }
    curProto = Object.getPrototypeOf(curProto);
  }
  return false;
};

手写数据类型判断函数

Object.prototype.toString调用与正则,需要注意的是call方法的调用与match方法的返回结果。

1
2
3
4
5
6
7
8
9
/**
 * @param {any} data
 * @return {string}
 */
const detectType = (data) =>
  Object.prototype.toString
    .call(data)
    .match(/^\[object (\w+)\]$/)[1]
    .toLowerCase();

手写 promisify 函数

  1. 注意promisify需要返回一个函数,这个函数返回一个promise对象。
  2. 注意 Node.js 中常规的回调函数形式,因此在改变this执行的时候使用call方法代码更加简洁。

    1
    fs.readFile("1.txt", "utf8", (err, data) => {});
  3. err的情况就先reject,也是利用了回调函数的特性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
     * @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 方法

asyncawait的语法其实是generator函数与promise结合的语法糖,通过promise实现自动执行最后实现协程的效果。

首先查看 generator 函数的基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function* helloWorldGenerator() {
  yield "hello";
  yield "world";
  return "ending";
  yield "what can i say";
}
const hw = helloWorldGenerator();

console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
console.log(hw.next());
// { value: 'hello', done: false }
// { value: 'world', done: false }
// { value: 'ending', done: true }
// { value: undefined, done: true }

function* dataConsumer() {
  console.log("Started");
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return "result";
}

const genObj = dataConsumer();
console.log(genObj.next());
console.log(genObj.next("a"));
console.log(genObj.next("b"));
// Started
// { value: undefined, done: false }
// 1. a
// { value: undefined, done: false }
// 2. b
// { value: 'result', done: true }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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函数可以将函数包装成另一个执行函数,在执行完这个函数后就移除掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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) {
    if (!this.callbacks[type]) {
      console.warn("没有订阅该事件!");
      return;
    }
    const index = this.callbacks[type].indexOf(callback);
    if (index > -1) {
      this.callbacks[type].splice(index, 1);
    }
  }
  off(type, callback) {
    this.removeListener(type, callback);
  }
  emit(type, ...args) {
    if (!this.callbacks[type]) {
      console.warn("没有订阅该事件!");
      return;
    }
    this.callbacks[type].forEach((callback) => callback.apply(this, args));
  }
  once(type, callback) {
    function wrapper(...params) {
      callback.apply(this, params);
      this.removeListener(type, wrapper);
    }
    this.addListener(type, wrapper);
  }
}
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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;
  function bound(...args) {
    const context = Boolean(new.target) ? this : obj;
    return 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);
  return 1;
}
let person = {
  age: 3,
};

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

手写 Object.create

利用new关键字,实例的__proto__属性会指向构造函数的prototype

  • new初始化一个对象的规范:如果构造函数的prototype不是一个对象,则将新创建的对象的__proto__设置为标准的内置的对象的原型对象,即Object.prototype
  • 在使用Object.create方法时,强制把这个对象的__proto__设置为该参数。目前该方法是将对象原型设置为空的唯一手段。
  • Class类的prototype属性默认不可写。
1
2
3
4
5
6
7
function Foo() {}
Foo.prototype = null;
console.log(new Foo().toString); //outputs function toString() { [native code] } (or whatever)

function Foo() {}
Foo.prototype = Object.create(null);
console.log(new Foo().toString); //output undefined
1
2
3
4
5
6
7
8
9
10
11
12
/**
 * @param {any} proto
 * @return {object}
 */
const myObjectCreate = (proto) => {
  if (typeof proto !== "object" || proto === null) {
    throw new TypeError("Argument must be an object!");
  }
  function A() {}
  A.prototype = proto;
  return new A();
};

手写数组 push

这种方法有点取巧,很多特性都是 JS 数组特有的行为,比如数组长度会随之数组的最大索引变化,没有值的数据会自动填充为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * Adds one or more elements to the end of an array and returns the new length of the array.
 *
 * @param {Array} arr - The target array to which elements will be added.
 * @param {...*} params - The elements to add to the array.
 * @returns {number} - The new length of the array after adding the elements.
 */
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方法,注意递归的核心为保证字符串不变

1
2
3
4
5
6
7
8
9
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强制转换对象会返回对象本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * @param {any} target
 * @param {any[]} sources
 * @return {object}
 */
const objectAssign = (target, ...sources) => {
  if (!target) {
    throw new TypeError(
      "TypeError: Cannot convert undefined or null to object"
    );
  }
  const ret = Object(target);
  sources.forEach((source) => {
    if (source) {
      source = Object(source);
      Reflect.ownKeys(source).forEach((key) => {
        if (Reflect.getOwnPropertyDescriptor(source, key).enumerable) {
          ret[key] = source[key];
          if (ret[key] !== source[key]) {
            throw new Error();
          }
        }
      });
    }
  });
  return ret;
};

const a = { a: 1 };
const b = { b: 2 };
const c = objectAssign(a, b);
console.log(a, b, c);

实现数组的 flat 方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * @param { Array } arr
 * @param { number } depth
 * @returns { Array }
 */
const flat = (arr, depth = 1) => {
  const res = [];
  const step = (array, count) => {
    if (!Array.isArray(array) || count < 0) {
      res.push(array);
      return;
    }
    for (const item of array) {
      step(item, count - 1);
    }
  };
  step(arr, depth);
  return res;
};

/**
 * @param { Array } arr
 * @param { number } depth
 * @returns { Array }
 */
const flat = (arr, depth = 1) =>
  depth > 0
    ? arr.reduce(
        (pre, cur) =>
          pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : [cur]),
        []
      )
    : arr;

深入 JS 的使用

柯里化

注意外层函数需要命名以便在递归的时候引用,内层函数则是内部也应该返回函数的相关结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
 * @param { (...args: any[]) => any } fn
 * @returns { (...args: any[]) => any }
 */
const curry = (fn) =>
  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) =>
  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();

深拷贝

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

Descriptors来全给你拷了。

1
2
3
4
5
const copy = (obj) =>
  Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj)
  );

采用for循环实现深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const cloneDeep = (data) => {
  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);
};

防抖与节流

防抖的复杂实现注意isInvoked的位置放在了返回函数的里面,这样可以保证初始执行的这一次不会触发trailing这一次的执行。有定时器就清除,无论之前有没有定时器都需要继续设置setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
 * @param {(...args: any[]) => any} func
 * @param {number} wait
 * @returns {(...args: any[]) => any}
 */
const debounce = (func, wait) => {
  let timer = null;
  return function (...params) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      func.apply(this, params);
      timer = null;
    }, wait);
  };
};

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

节流的复杂实现在于实现trailing的功能,这边涉及到递归函数的提取。注意之前有定时器的话就只保存参数什么都不做,没定时器才要设置setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
 * @param {(...args:any[]) => any} func
 * @param {number} wait
 * @returns {(...args:any[]) => any}
 */
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 = () => {
    timer = null;
    if (option.trailing && lastArgs) {
      func.apply(this, lastArgs);
      lastArgs = null;
      timer = setTimeout(run, wait);
    }
  };
  return function (...params) {
    if (!timer) {
      if (option.leading) {
        func.apply(this, params);
      }
      timer = setTimeout(run, wait);
    } else {
      lastArgs = params;
    }
  };
};

并发控制

并发池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const asyncPool = async (poolLimit, array, iteratorFn) => {
  const ret = [],
    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);
};

const timeout = (i) =>
  new Promise((resolve) =>
    setTimeout(() => {
      console.log(i);
      resolve(i);
    }, i)
  );

asyncPool(2, [1000, 5000, 3000, 2000], timeout);

递归版本,也叫能够实现并发控制的Promise.all版本。注意while循环的巧妙使用,先将能填进去的数组先填进去。为了严格与Promise.all方法保持一致,这里做到了索引与数组直接相同。同时还需要注意递归调用的重要性。同时还有万能并发池的版本,有点缺陷的地方在于数组splice方法的时间复杂度部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
 * @param {Array<() => Promise<any>>} funcs
 * @param {number} max
 * @return {Promise}
 */
const throttlePromises = (funcs, max = Infinity) => {
  return new Promise((resolve, reject) => {
    const length = funcs.length;
    const res = new Array(length);
    let count = 0,
      running = 0,
      index = 0;
    const run = () => {
      while (funcs.length && running < max) {
        const func = funcs.shift();
        const idx = index;
        func().then((v) => {
          res[idx] = v;
          running--;
          count++;
          if (count === length) {
            resolve(res);
          }
          run();
        }, reject);
        running++;
        index++;
      }
    };
    run();
  });
};

/**
 * @param {Array<() => Promise<any>>} funcs
 * @param {number} max
 * @return {Promise}
 */
const throttlePromises = async (funcs, max = Infinity) => {
  const ret = [],
    executing = [];
  for (const func of funcs) {
    const p = Promise.resolve(func());
    ret.push(p);
    if (funcs.length > max) {
      const e = p.finally(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= max) {
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(ret);
};

调度器版本

难绷,曾经最熟悉的一题,居然没做出来。🤡 卡点主要在于add方法需要返回一个promise对象,这个promise对象的状态应该由task函数的执行结果来决定,但根据并发控制的需求,不能直接执行task函数,这样违背了并发控制的需求,需要将这个函数再包装一下,包装成另一个函数即() => task().then(resolve, reject),推入任务队列,其他即为递归的调度执行。注意由于任务是一个个加进来的所以使用的是if判断条件,和上面的还是有一些不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Scheduler {
  constructor(size = 2) {
    this.size = size;
    this.queue = [];
    this.running = 0;
  }
  add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push(() => task().then(resolve, reject));
      this.executeNext();
    });
  }
  executeNext() {
    if (this.queue.length && this.running < this.size) {
      const task = this.queue.shift();
      task().finally(() => {
        this.running--;
        this.executeNext();
      });
      this.running++;
    }
  }
}

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

lodash.get

基本思路为解析路径至数组然后使用数组的reduce方法进行递归获取对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * @param {object} source
 * @param {string | string[]} path
 * @param {any} [defaultValue]
 * @return {any}
 */
const get = (source, path, defaultValue = undefined) => {
  if (!Array.isArray(path)) {
    path = path.replace(/\[(\w+)\]/g, ".$1").split(".");
  }
  if (path.length === 0) {
    return defaultValue;
  }
  const res = path.reduce((pre, cur) => pre[cur], source);
  return res ?? defaultValue;
};

lodash.set

这个比较复杂一些,需要注意的细节有点多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * @param {object} obj
 * @param {string | string[]} path
 * @param {any} value
 */
const set = (obj, path, value) => {
  if (!Array.isArray(path)) {
    path = path.replace(/\[(\w+)\]/g, ".$1").split(".");
  }
  const isNumber = (str) => str === String(Number(str));
  path.reduce((pre, cur, index) => {
    if (index === path.length - 1) {
      pre[cur] = value;
    }
    if (!pre[cur]) {
      if (isNumber(path[index + 1])) {
        pre[cur] = [];
      } else {
        pre[cur] = {};
      }
    }
    return pre[cur];
  }, obj);
};

千分位格式化

两种方式,循环与正则,推荐正则,注意正先行断言-存在使用的一些注意事项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * @param {number} num
 * @return {string}
 */
const addComma = (num) => {
  const [p, q] = String(num).split(".");
  let count = 0,
    res = "";
  const regex = /[0-9]/;
  for (let i = p.length - 1; i >= 0; i--) {
    res = p[i] + res;
    if (++count % 3 === 0 && regex.test(p[i - 1])) {
      res = "," + res;
    }
  }
  if (!q) {
    return res;
  }
  return res + "." + q;
};

/**
 * @param {number} num
 * @return {string}
 */
const addComma = (num) => {
  let [p, q] = String(num).split(".");
  p = p.replace(/(\d)(?=(\d{3})+$)/g, "$1,");
  if (!q) {
    return p;
  }
  return p + "." + q;
};

console.log(addComma(-10000000000.102));

LazyMan

很经典的手写题,值得注意的地方有:

  1. 维护一个tasks队列存储所有任务。
  2. 使用setTimeout函数将任务推入宏任务队列,并且使用asyncawait语法配合for循环控制任务的执行顺序。
  3. 返回一个对象,对象身上有相关的执行方法,每个执行方法都返回对象本身,实现链式调用,注意相关方法不能使用箭头函数,这样this指向会出现问题,不会指向返回的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 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;
    },
  };
};

const HardMan = (name) => {
  const tasks = [() => console.log(`I am ${name}`)];
  const rest = (i) =>
    new Promise((resolve) => setTimeout(resolve, i * 1000)).then(() =>
      console.log(`Start learning after ${i} seconds`)
    );
  const learn = (subject) => console.log(`Learning ${subject}`);
  setTimeout(async () => {
    for (const task of tasks) {
      await task();
    }
  });
  return {
    rest(i) {
      tasks.push(() => rest(i));
      return this;
    },
    restFirst(i) {
      tasks.unshift(() => rest(i));
      return this;
    },
    learn(subject) {
      tasks.push(() => learn(subject));
      return this;
    },
  };
};

// HardMan("jack");
// I am jack

// HardMan("jack").rest(10).learn("computer");
// 输出
// I am jack
// 等待10秒
// Start learning after 10 seconds
// Learning computer

// HardMan("jack").restFirst(5).learn("chinese");
// 输出
// 等待5秒
// Start learning after 5 seconds
// I am jack
// Learning chinese

数组转树

JSON文件转化为树结构的问题,关键在于利用对象的引用和map结构来进行操作。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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();
  for (const item of arr) {
    map.set(item.id, item);
  }
  let res = null;
  for (const item of arr) {
    if (item.pid === 0) {
      res = item;
      continue;
    }
    const parent = map.get(item.pid);
    if (!parent.children) {
      parent.children = [];
    }
    parent.children.push(item);
  }
  return res;
};
console.dir(convertToTree(arr), { depth: null });

// {
//   id: 1,
//   name: '部门1',
//   pid: 0,
//   children: [
//     { id: 2, name: '部门2', pid: 1 },
//     {
//       id: 3,
//       name: '部门3',
//       pid: 1,
//       children: [
//         {
//           id: 4,
//           name: '部门4',
//           pid: 3,
//           children: [ { id: 5, name: '部门5', pid: 4 } ]
//         }
//       ]
//     }
//   ]
// }

斐波那契数列的两种实现方式

尾调用优化解决问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fib = (n, a = 0, b = 1) => {
  if (n === 0) {
    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

业务场景题

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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();
  2. 使用promise封装一个ajax请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <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>
  3. 实现日期格式化函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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日
  4. 交换a, b的值不能用临时变量

    1
    2
    3
    b = a + b;
    a = b - a;
    b = b - a;
  5. 注意ajax一定需要send方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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;
    }
  6. 数组的乱序输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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. 实现数组的斜向打印

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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. 电话号码的打印:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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. 循环打印方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    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. 丢手帕问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    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. 查找文章中出现频率最高的单词

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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. 判断对象中是否存在循环引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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. 手写一个undefinedToNull函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
     * @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. 判断字符串的有效数字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
     * @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. 实现一个累加器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
     * @param {number} num
     */
    const sum = (count) => {
      function sumInner(number) {
        return sum(count + number);
      }
      sumInner.valueOf = () => count;
      return sumInner;
    };
  17. counter function自执行包裹

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const count = (() => {
      let num = 0;
      function func() {
        return ++num;
      }
      func.reset = () => {
        num = 0;
      };
      return func;
    })();
  18. counter对象,简单的数据代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
     * @returns { {count: number}}
     */
    const createCounter = () => {
      let count = 0;
      return Object.defineProperty({}, "count", {
        get() {
          return count++;
        },
        set() {
          console.log("it cannot be altered");
        },
      });
    };
  19. 失败后自动发起请求,超时后停止。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
     * @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) => {
            count++;
            if (count > maximumRetryCount) return reject(r);
            run();
          });
        };
        run();
      });
    }
  20. 封装一个fetch请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    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;
      }
    }