JavaScript手写题(一)
这个系列是关于前端面试常见的 js 手写题的,对于每一道题而言,可能会有很多不同的解决思路。所以这种面试题背是没有用的,复习的关键在于掌握解决问题的出发点。现实中面临的有些业务场景可比这些面试题复杂的多,所以练习一下一些基本功能的实现比如lodash、ESAPI等的实现,对编码能力是很有帮助的。
实现一个 compose 函数
实现一个compose函数,进行函数合成,如:
const add10 = (x) => x + 10;
const mul10 = (x) => x * 10;
const add100 = (x) => x + 100;
// (10 + 100) * 10 + 10 = 1110
compose(add10, mul10, add100)(10);通过分析用例我们可以看到,compose函数调用返回一个函数,当调用这个返回的函数时,传入的参数会被先前传入compose函数的参数列表从右往左处理。
最简版实现可以利用reduce来做:
也可以像koa-compose那样用尾递归做:
在lodash库中的flowRight中也实现了这个功能,源码如下:
function flow(...funcs) {
const length = funcs.length;
let index = length;
while (index--) {
if (typeof funcs[index] !== "function") {
throw new TypeError("Expected a function");
}
}
return function (...args) {
let index = 0;
let result = length ? funcs[index].apply(this, args) : args[0];
while (++index < length) {
result = funcs[index].call(this, result);
}
return result;
};
}
function flowRight(...funcs) {
return flow(...funcs.reverse());
}实现一个深拷贝 cloneDeep
所谓深拷贝,就是根据源对象复制一个对象出来,且对新对象的任意更改不会引起旧对象的变化。
简单实现如下:
这种简单的实现虽然基本够用,不过有两个问题:
没有处理传入值为数组的情况
没有处理循环引用的问题
所谓循环引用,可以看看以下用例:
const a = {
a: 1,
b: [1, 2, 3],
};
a.a = a;
var d = cloneDeep(d);
console.log(a, d);如果采用前面的cloneDeep实现,会报以下错误:
RangeError: Maximum call stack size exceeded
当然,报错是很显然的,因为对一个存在循环引用的对象进行递归,本就是个无法穷尽的过程。因此我们要改良上述代码,对这种情形进行规避。
改完后的代码是这样的(与其说是改良,不如说是重新写了):
function isBasicType(a: any): boolean {
return typeof a !== "object";
}
function cloneDeep(val: any, memo = new Map()) {
if (isBasicType(val)) return val;
if (memo.get(val)) return memo.get(val);
let i = 0;
const _isArray = Array.isArray(val);
const res: any = _isArray ? [] : {};
const keys = Object.keys(val);
for (i = 0; i < keys.length; i++) {
const _val = val[keys[i]];
if (_val === val) {
memo.set(val, res);
res[keys[i]] = res;
} else {
if (_isArray) {
res.push(cloneDeep(_val));
} else {
res[keys[i]] = cloneDeep(_val, memo);
}
}
}
return res;
}
const a: any = {
a: 1,
b: [1, 2, 3],
};
a.a = a;
var d = cloneDeep(a);
console.log(a, d);提示
由于TypeScript PlayGround似乎没法打印循环引用的对象,所以上面只贴了代码。
上面代码规避循环引用的方式是用哈希表存储旧引用经过clone后的新引用,把旧引用作为哈希表的key。如果遍历的值在哈希表中存在,就直接从哈希表里面拿,把对应的新引用赋值过去。上面的代码除了解决循环引用的问题,还增加了对数组类型的适配。
当然,这个实现还远远不够,所以日常使用还是建议直接用lodash的cloneDeep,因为人家把能想到的所有情形都放进去了,因为源码比较多,就不贴进来了。
实现一个防抖/节流函数
防抖和节流函数也是在实际生产用的比较多的功能了,在面试中作为闭包的常见应用进行考察。两个函数核心实现都是利用闭包对timer进行暂存,防抖的话在时间未到时会重置计时器,节流则是在时间未到时不进行操作。
代码如下:
- 防抖:
- 节流:
lodash中对这两个功能的实现则更加复杂而全面,详见:手撕源码系列 —— lodash 的 debounce 与 throttle
实现一个异步的 sum/add
本题摘自【Q644】实现一个异步的 sum/add · Issue #662 · shfshanyue/Daily-Question (github.com)。这道题是字节跳动的面试题,质量很高,考察了和异步相关的很多东西。
题目:请实现以下 sum 函数,只能调用 add 进行实现:
/*
请实现一个 sum 函数,接收一个数组 arr 进行累加,并且只能使用add异步方法
add 函数已实现,模拟异步请求后端返回一个相加后的值
*/
function add(a, b) {
return Promise.resolve(a + b);
}
function sum(arr) {}初级:串行相加
因为add的返回值为Promise,所以串行相加要求我们在每一次Promise的then中进行相加操作:
function sum(arr) {
if (arr.length === 1) return arr[0];
return arr.reduce((x, y) => Promise.resolve(x).then((x) => add(x, y)));
}也可以用ES7的async/await来表示这个过程:
async function sum(arr) {
let s = arr[0];
for (let i = 1; i < arr.length; i++) {
s = await add(s, arr[i]);
}
return s;
}注意:不能用 Array 的 forEach 进行遍历,因为不能保证每一趟执行都在上一次add完结之后。
串行带来的问题:如果单次计算耗时较长,数据量大时耗时就很长了。
中级:分组并行(chunk)
我们将数组进行两两分组相加,这样以后对得到的数组再进行同样的操作,如此往复,最终得出相加的结果。这种做法会比串行的耗时更少。
实现数组分组的chunk函数代码如下:
function chunk(list = [], size) {
return list.reduce((pre, cur) => {
const length = pre.length;
if (length === 0 || pre[length - 1].length === size)
return pre.concat([[cur]]);
else {
pre[length - 1].push(cur);
return pre;
}
}, []);
}通过使用chunk改良后的sum代码:
async function sum(arr) {
if (arr.length === 1) return arr[0];
const promises = chunk(arr, 2).map(([x, y]) =>
// 注意此时单数的情况
y === undefined ? x : add(x, y)
);
return Promise.all(promises).then((list) => sum(list));
}这里还有一个问题,上面代码的promises数组的长度如果很大,意味着单位时间并发数会非常大。这里就涉及到并发数控制的问题了。
高级:并发数控制
现在我们要实现一个函数pMap实现并发数控制,pMap有如下结构:
type pMap = (
argsArr: any[],
fn: (...args: any) => any,
concurrency?: number
) => Promise;其中concurrency为并发数,即最多只能有这么多个任务同时运行。
关于这个功能的实现,有几个值得注意的点:
返回值为
Promise,可以规划出大致的代码框架:const pMap: pMapType = (argsArr, fn, concurrency = Infinity) => { return new Promise((resolve, reject) => { //... }); };最终返回的
Promise应当是包含所有结果的数组,这里有几个注意的点:每个任务的开始时机和完结时机不尽相同,不过返回的数组中的结果要按
argsArr的顺序一一对应排列当一个任务完结时,如果此时同时进行的任务尚未达到并发限制需要马上进行新任务的添加。所以在过程执行的每个
Promise的then中要有添加任务的逻辑错误处理:如果其中一个任务出错直接
reject
代码实现如下:
设计完pMap后,回到我们刚才的问题,异步相加的代码可以是这样的:
async function sum(arr, concurrency) {
if (arr.length === 1) return arr[0];
return pMap(
chunk(arr, 2),
([x, y]) => {
return y === undefined ? x : add(x, y);
},
concurrency
).then((list) => sum(list, concurrency));
}实现一个无限累加的 sum 函数
题目:实现一个 sum 函数如下所示:
sum(1, 2, 3).valueOf() //6
sum(2, 3)(2).valueOf() //7
sum(1)(2)(3)(4).valueOf() //10
sum(2)(4, 1)(2).valueOf() //9
sum(1)(2)(3)(4)(5)(6).valueOf() // 21解法1:直接算
所谓直接算,即每调用一次算一次,结果利用闭包来存储。
返回值为一个函数,在上面挂载valueOf方法输出当前缓存的值即可。
代码实现如下:
解法2:懒计算
懒计算的思路还是不错的,即把计算环节放到了valueOf来做。
代码实现如下:
