JS手写题大汇总
本文主要记录了关于前端面试常考的手写代码题,常看常复习。
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
关键字到底干了什么。
- 创建一个空对象将该对象的原型设置为函数的
prototype
。 - 执行函数,
this
指向指向该对象。 - 若函数返回结果不为空且为对象,则返回该对象,否则返回先前生成的对象。
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 函数
- 注意
promisify
需要返回一个函数,这个函数返回一个promise
对象。 注意 Node.js 中常规的回调函数形式,因此在改变
this
执行的时候使用call
方法代码更加简洁。1
fs.readFile("1.txt", "utf8", (err, data) => {});
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 方法
async
与await
的语法其实是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
很经典的手写题,值得注意的地方有:
- 维护一个
tasks
队列存储所有任务。 - 使用
setTimeout
函数将任务推入宏任务队列,并且使用async
和await
语法配合for
循环控制任务的执行顺序。 - 返回一个对象,对象身上有相关的执行方法,每个执行方法都返回对象本身,实现链式调用,注意相关方法不能使用箭头函数,这样
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
业务场景题
利用迭代器来使
async
与await
来阻塞当前函数的执行线程。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const 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();
使用
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>
实现日期格式化函数
1
2
3
4
5
6
7
8
9
10
11
12
13const 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日
交换
a, b
的值不能用临时变量1
2
3b = a + b; a = b - a; b = b - a;
注意
ajax
一定需要send
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function 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; }
数组的乱序输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const 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);
实现数组的斜向打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function 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], ]) );
电话号码的打印:
1
2
3
4
5
6
7
8
9
10
11
12
13const 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"));
循环打印方案
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
64const 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);
丢手帕问题
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
54const 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));
查找文章中出现频率最高的单词
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
47const 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"));
setTimeout
模仿setInterval
1
2
3
4
5
6
7
8
9
10
11
12
13const 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);
判断对象中是否存在循环引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const 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));
手写一个
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; };
判断字符串的有效数字
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); };
实现一个累加器
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; };
counter
function
自执行包裹1
2
3
4
5
6
7
8
9
10const count = (() => { let num = 0; function func() { return ++num; } func.reset = () => { num = 0; }; return func; })();
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"); }, }); };
失败后自动发起请求,超时后停止。
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(); }); }
封装一个
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
40class 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; } }