本文详细解析 JavaScript 面试中常见的手写题目,重点讲解柯里化函数与数组扁平化的实现原理。通过闭包、递归、栈及 ES6 新特性等多种方式,深入剖析柯里化的参数收集机制与数组扁平化的不同处理策略,帮助开发者巩固前端基础知识。
以下题目源自其他技术博客,但解答均基于笔者个人理解整理。如果你近期有换工作打算,或希望巩固前端知识基础,不妨加入每日刷题的行列,一起提升技能。
第四天需要完成的手写题目如下:
- 手写一个函数,实现柯里化功能
- 手写一个函数,实现数组扁平化的功能
- 手写一个函数,实现数组去重的功能
以下是笔者自己编写的参考答案:
1. 手写一个函数,实现柯里化功能
分析:柯里化的核心在于**收集到足够数量的参数(更准确地说,是生产资料)**后,才执行预设函数。若当前参数数量不足,则不执行,而是继续收集参数。
如果用代码来表示,可以设既定函数为f,柯里化之后映射成为函数g,g在执行的时候会对积累到的参数的数目进行判断,如果数目是足够的,则返回f的执行结果(相当于执行了f),如果参数的数目不够,则将此次调用传入的参数累积起来,然后返回函数g',等待下一次调用。**返回的g'函数和g具有相同的德行。
基于上述分析,首先需要一个私有域来存储已收集的参数,这需要借助闭包实现;而执行预设函数所需的参数数量,可以直接从预设函数本身获取:f.length表示函数需要的形参数量。
现在尝试编写代码:
function _G (f) {
if(typeof f !== "function") throw new Error('f must be a function!');
return function (...rest) {
const currentParams = [...rest];
const currentLength = currentParams.length;
if(currentLength >= f.length) return f.apply(this, currentParams);
// return _G(f); ???
}
}
上述代码无法处理参数不足时如何返回一个g'的问题。前文分析指出g'与g性质相同,因此g'与g均由_G产生,问题出在_G的设计上,仅有一个参数无法区分g'与g。
因此,需要增加一个形参,用于表示目前g类已收集到的参数:
function _G (f, alreadyCollectedParams = []) {
if(typeof f !== "function") throw new Error('f must be a function!');
return function (...rest) {
const currentParams = [...alreadyCollectedParams, ...rest];
const currentLength = currentParams.length;
if(currentLength >= f.length) return f.apply(this, currentParams);
return _G(f, currentParams);
}
}
function add (a,b,c) {
return a+b+c;
}
const c_add = _G(add);
console.log(c_add(1)(2)(3)); // 6
console.log(c_add(1, 2)(3)); // 6
console.log(c_add(1)(2, 3)); // 6
console.log(c_add(1, 2, 3)); // 6
使用 ES6 语法创建极简版本:
// 极简版
// const __G = (f, alreadyCollectedParams = []) => (...rest) => [...alreadyCollectedParams, ...rest].length >= f.length ? f.apply(this, [...alreadyCollectedParams, ...rest]) : _G(f, currentParams);
const __G = (f, a = []) => (...r) => (_ = [...a, ...r], _.length >= f.length ? f.apply(this, _) : __G(f, _));
2. 手写一个函数,实现数组扁平化的功能
所谓数组扁平化,是指如果数组中的元素仍然是数组,则将内嵌数组中的元素提取出来,直接放置到上层数组中。
2.1 方法一:forEach 和 push
最基础的想法是,创建一个_flat方法用于遍历数组,并在遍历过程中对每个元素进行类型判断;如果元素类型不是数组,则直接 push 到记录数组中;如果元素类型是数组,则对该内嵌数组递归调用_flat。这相当于对任意层级数组的每个元素进行遍历,即使用深度优先遍历算法。
function _flat (targetArray, container = []) {
if(!Array.isArray(targetArray)) return container;
targetArray.forEach(
item => {
if (!Array.isArray(item)) {
container.push(item);
} else {
_flat(item, container);
}
}
)
return container;
}
const rst = _flat([[[[[[1],2],3],4],5,6],7]);
// const rst = _flat('[[[[[[1],2],3],4],5,6],7]');
console.log('rst: ', rst);
2.2 方法二: Array.prototype.flat
ES6 中 Array 原型上增加了一个名为 flat 的方法,其作用是将嵌套数组拆包一层;显然,每拆包一次,整个数组的元素数量是(非严格)单调递增的。利用这一性质,使用 while 循环持续对其拆包,直到某两次拆包后的元素数量相等为止。
function _flat2 (targetArray) {
if(!Array.isArray(targetArray)) return [];
let _loop = targetArray;
while(1){
const beforeFlat = _loop.length;
const _Arr = _loop.flat();
const afterFlat = _Arr.length;
if(beforeFlat == afterFlat) return _Arr;
_loop = _Arr;
}
}
const rst2 = _flat2([[[[[[1],2],3],4],5,6],7]);
console.log('rst2: ', rst2);
2.3 方法三: findIndex 和 splice
如果在遍历前就能知道内嵌数组元素的序列号,只需找到对应位置并将其展开即可;该过程持续进行,直到原数组中再也找不到内嵌数组元素时停止。这种方法直接在原数组上操作,会改变原数组的内容。
function _flat3 (targetArray) {
if(!Array.isArray(targetArray)) return [];
while(1){
const arrItemIndex = targetArray.findIndex(
item => Array.isArray(item)
)
if(arrItemIndex === -1) return targetArray;
targetArray.splice(arrItemIndex, 1, ...targetArray[arrItemIndex]);
}
}
const rst3 = _flat3([[[[[[1],2],3],4],5,6],7]);
console.log('rst3: ', rst3);
2.4 方法四: stack
- 使用栈这种数据结构,其本质与递归算法完全相同;但使用栈来理解,会极大降低心智负担。
- 使用两个栈 a 和 b,开始时将原始数组整体放入到 a 栈中,此时 b 栈为空;
- 然后对 a 栈执行以下动作,直到 a 栈为空:
- 弹栈 -> 判断弹出元素是否是数组,如果不是,则入栈 b;如果是,则拆包一次,再重新入栈 a
- 最后输出栈 b 即可
function _flat4 (targetArray) {
if(!Array.isArray(targetArray)) return [];
// 原始数组全部入a栈
const a = [...targetArray];
const b = [];
while(a.length){
const _tmp = a.pop();
if (Array.isArray(_tmp)) {
a.push(..._tmp); // 这里不要遍历push,显得很low,a.concat(_tmp)也可
} else {
b.push(_tmp);
}
}
return b;
}
const rst4 = _flat4([[[[[[1],2],3],4],5,6],7]);
console.log('rst4: ', rst4);
3. 手写一个函数,实现数组去重的功能
即字面意思,不难理解!
3.1 利用 set 对象的机制,将数组先变成 set 然后将 set 再变成数组
// 一行搞定
const unique = (array) => [...newSet(array)];
3.2 继续使用两个栈
- 继续使用两个栈 a 和 b,a 执行以下动作直到空栈
- 弹栈 -> 判断弹出元素在 b 栈中是否存在,如果存在丢弃,如果不存在进入栈 b
const unique2 = (array) => {
if(!Array.isArray(array)) return [];
const a = [...array];
const b = [];
while(a.length){
const item = a.pop();
// const item = a.shift();
if(b.includes(item)) continue;
b.push(item);
}
return b;
}
可以将 pop 改成 shift 有利于保证顺序
3.3 3.2 改进版本
对上面的实现方式进行优化,因为数组通过内容查询元素的效率实在太低,所以将 b 从栈改成字典,字典一般是使用 hash 表实现的,在根据查找方面比数组要快
const unique3 = (array) => {
if(!Array.isArray(array)) return [];
const a = [...array];
const b = new Map();
while(a.length){
const item = a.pop();
// const item = a.shift();
b.set(item,1);
}
return [...b.keys()];
}
console.log(unique3([1,1,1,1,2,3,4,345,345]));
到此这篇关于 JavaScript 常考手写题之柯里化与数组扁平化的实现的文章就介绍到这了,更多相关 JavaScript 手写题内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!