JS手写题大汇总

||
字数总计:5.9k|阅读时长:30分钟|阅读量:…
文章最后更新于 895 天前,其中的信息可能已经过时。

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

JS 基础篇

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

ES5 实现继承

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

  • 使用Object.create方法,构造以父类的prototype为原型的新对象,防止对子类原型对象的修改影响到父类的原型对象。
  • 注意静态方法的继承,使用Object.setPrototypeOf将父类设置为子类的原型。
JS
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();

验证的例子:

JS
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. 若函数返回结果不为空且为对象,则返回该对象,否则返回先前生成的对象。
JS
/**
* @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。

JS
/**
* @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方法的返回结果。

JS
/**
* @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方法代码更加简洁。

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

    JS
    /**
    * @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 函数的基本使用
JS
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 }
JS
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函数可以将函数包装成另一个执行函数,在执行完这个函数后就移除掉。
JS
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关键字还做作为对象调用父对象上的方法或者静态方法,但在普通方法里面只能调用普通方法而不能调用静态方法,但在子类的静态方法中则可以都可以调用。

JS
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属性默认不可写。
JS
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
JS
/**
* @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 数组特有的行为,比如数组长度会随之数组的最大索引变化,没有值的数据会自动填充为空。

JS
/**
* 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方法,注意递归的核心为保证字符串不变

JS
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强制转换对象会返回对象本身。

JS
/**
* @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方法能减少不少代码量。

JS
/**
* @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 的使用

柯里化

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

JS
/**
* @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来全给你拷了。

JS
const copy = (obj) =>
Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);

采用for循环实现深拷贝

JS
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。

JS
/**
* @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。

JS
/**
* @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;
}
};
};

并发控制

并发池

JS
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方法的时间复杂度部分。

JS
/**
* @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判断条件,和上面的还是有一些不一样。

JS
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方法进行递归获取对象

JS
/**
* @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

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

JS
/**
* @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);
};

千分位格式化

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

JS
/**
* @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函数将任务推入宏任务队列,并且使用async和await语法配合for循环控制任务的执行顺序。
  3. 返回一个对象,对象身上有相关的执行方法,每个执行方法都返回对象本身,实现链式调用,注意相关方法不能使用箭头函数,这样this指向会出现问题,不会指向返回的对象。
JS
// 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,然后遍历对象进行赋值就好了,主要还是利用了对象本身的唯一性。

JS
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 } ]
// }
// ]
// }
// ]
// }

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

尾调用优化解决问题。

JS
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. 利用迭代器来使async与await来阻塞当前函数的执行线程。

    JS
    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请求

    HTML
    <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. 实现日期格式化函数

    JS
    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的值不能用临时变量

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

    JS
    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. 数组的乱序输出

    JS
    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. 实现数组的斜向打印

    JS
    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. 电话号码的打印:

    JS
    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. 循环打印方案

    JS
    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. 丢手帕问题

    JS
    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. 查找文章中出现频率最高的单词

    JS
    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

    JS
    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. 判断对象中是否存在循环引用

    JS
    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函数

    JS
    /**
    * @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. 判断字符串的有效数字

    JS
    /**
    * @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. 实现一个累加器

    JS
    /**
    * @param {number} num
    */
    const sum = (count) => {
    function sumInner(number) {
    return sum(count + number);
    }
    sumInner.valueOf = () => count;
    return sumInner;
    };
  17. counter function自执行包裹

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

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

    JS
    /**
    * @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请求

    JS
    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;
    }
    }

评论

评论加载中…

站内搜索