在前端面试过程中,免不了一些与业务相挂钩的手写题,这些题目有时候考得比算法还多,毕竟作为前端来说,接触算法还是相对少一些。最近在面试,也是梳理了一波这种题目。接下来我会盘点面试经常遇到的手写题以及解决思路,题目来自于各个网站,时间比较久远了忘记引用了哪些链接了。
梳理成了一版类似于问答的编程版本,感兴趣的可以给个star github.com/wuquan1995/...
1. 版本比较
样例输入:versions = ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5'] 期望输出:['0.1.1', '0.302.1', '2.3.3', '4.3.4.5', '4.3.5']
核心思路:使用sort方法对数组元素两两比较,回调函数里面主要要体现分位比较的过程。
JavaScript
function compareVersion(arr){
let copyArr = [...arr];
copyArr.sort((a,b)=>{
const tempA = a.split('.');
const tempB = b.split('.');
const maxLen = Math.max(tempA.length, tempB.length);
for(let i = 0; i < maxLen;i++){
const targetA = +tempA[i] || 0;
const targetB = +tempB[i] || 0;
if(targetA === targetB) continue
return targetA - targetB
}
return 0;
})
return copyArr;
}
测试案例
ini
let versions = ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']
let copyArr = compareVersion(versions);
console.log("copyArr",copyArr);
console.log("versions",versions);
2. 父子菜单数据结构转换
2.1 树转数组
JavaScript
// 输入
const tree = {
id: 1,
name: "部门A",
children: [
{
id: 2,
name: "部门B",
}
]
}
// 期望转换
[{id:1,name:'部门A',parentId:null}, {id:2,name:'部门B',parentId:1 }
核心思路:利用层次遍历,在每一层不断收集该节点进入队列,并为后续的子节点补充上parentId
JavaScript
function treeToArray(tree){
let result = [];
let queue = [];
result.push({ id:tree.id,name:tree.name, parentId:0});
queue.unshift({id:tree.id,name:tree.name,children:tree.children})
while(queue.length > 0 ){
const currentNode = queue.shift();
if(currentNode.children.length > 0){
for(let i = 0; i < currentNode.children.length;i++){
const childNode = currentNode.children[i];
const {id ,name } = childNode;
queue.unshift(childNode);
result.push({
id,
name,
parentId:currentNode.id
})
}
}
}
return result;
}
const tree = {
id: 1,
name: "部门A",
children: [
{
id: 2,
name: "部门B",
children: [
{
id: 4,
name: "部门D",
children: [
{
id: 7,
name: "部门G",
children: [
],
},
{
id: 8,
name: "部门H",
children: [
],
},
],
},
{
id: 5,
name: "部门E",
children: [
],
},
],
},
{
id: 3,
name: "部门C",
children: [
{
id: 6,
name: "部门F",
children: [
],
},
],
},
],
}
console.log(treeToArray(tree));
2.2 数组转树
预期效果即是将上面的输入输出翻转一下。
核心思路: 先用map收集一轮id与name的对应关系,后续采用"回溯"的思路为父节点添加子节点。
JavaScript
function arrayToTree(arr){
let map = new Map()
let result = null;
for(let i = 0; i < arr.length; i++){
const { id , name } = arr[i]
const children = [];
map.set( id, { id, name, children})
}
for(let i = 0; i < arr.length;i++){
const { parentId , id } = arr[i]
if(parentId !== 0){
const parentNode = map.get(parentId);
parentNode.children.push(map.get(id));
}else{
result = map.get(id);
}
}
return result;
}
const arr = [
{id:1, name: '部门A', parentId: 0},
{id:2, name: '部门B', parentId: 1},
{id:3, name: '部门C', parentId: 1},
{id:4, name: '部门D', parentId: 2},
{id:5, name: '部门E', parentId: 2},
{id:6, name: '部门F', parentId: 3},
{id:7, name: '部门G', parentId: 4},
{id:8, name: '部门H', parentId: 4}
]
console.log(arrayToTree(arr))
3. 编译DOM
JavaScript
const vDom = {
tag: 'DIV',
attrs:{
id:'app'
},
children: [
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] }
]
},
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] },
{ tag: 'A', children: [] }
]
}
]
};
// 期望转换成实际的DOM结构
console.log(_render(vDom))
核心思路:使用递归创建每个DOM节点及其子节点
JavaScript
// 真正的渲染函数
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === "number") {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
});
}
// 子数组进行递归操作 这一步是关键
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
const vDom = {
tag: 'DIV',
attrs:{
id:'app'
},
children: [
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] }
]
},
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] },
{ tag: 'A', children: [] }
]
}
]
};
// 把上述虚拟Dom转化成下方真实Dom
const realDom = `<div id="app">
<span>
<a></a>
</span>
<span>
<a></a>
<a></a>
</span>
</div>`;
console.log(_render(vDom))
4. 一维数组展开
输入: let arr = [1, [2, [3, 4, 5]]]; 期望输出: [1, 2, 3, 4,5]
核心思路:递归判断每个数组元素本身是否为数组,并往结果里面添加单数组元素,也可以采用reduce去优化下写法。
JavaScript
let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
let result = [];
for(let i = 0; i < arr.length; i++) {
const item = arr[i];
if(Array.isArray(item)) {
result = result.concat(flatten(item));
} else {
result.push(item);
}
}
return result;
}
flatten(arr); // [1, 2, 3, 4,5]
5. 延迟输出结果
调用方式:(new LazyLog()).log(1).sleep(1000).log(2) 期望输出:先输出1,延迟1秒后输出2
核心思路:创建一个队列用于存储任务,后续从队列里面取出任务同步执行。
JavaScript
class LazyLog {
constructor(){
this.queue = []
setTimeout(()=>{
this.run()
},0)
}
async run(){
for(let task of this.queue){
await task();
}
}
log(value){
this.queue.push(()=>{
console.log(value)
})
return this;
}
sleep(time){
this.queue.push(()=>{
return new Promise((resolve)=>{
setTimeout(resolve,time)
})
})
return this;
}
}
console.log(new LazyLog().log(1).sleep(1000).log(2));
6. 模拟并发请求
模拟network面板,一次最大只能发N个请求,期望能使用以下案例跑通。
JavaScript
// 这个函数是模拟发送请求的,实际中你可能需要替换成真实的请求操作
function sendRequest(url) {
console.log(`Sending request to ${url}`);
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Response received from ${url}`);
resolve(`Result from ${url}`);
}, Math.random() * 2000); // 随机延时以模拟请求处理时间
});
}
// 使用 RequestQueue
const requestQueue = new RequestQueue(3); // 假设我们限制最大并发数为3
// 模拟批量请求
const urls = ['url1', 'url2', 'url3', 'url4', 'url5', 'url6'];
urls.forEach(url => {
requestQueue.enqueue(url).then(result => {
console.log(result);
});
});
核心思路:创建一个RequestQueue类,并创建一个存储任务的队列。每次进队列时将当前运作的请求数加1,如果当前的请求数小于最大可并发数,则执行任务。
JavaScript
class RequestQueue {
constructor(maxConcurrent) {
this.maxConcurrent = maxConcurrent; // 设置最大并发数
this.currentRunning = 0; // 当前正在运行的请求数
this.queue = []; // 等待执行的请求队列
}
// 将请求封装成一个函数,推入队列,并尝试执行
enqueue(url) {
return new Promise((resolve, reject) => {
const task = () => {
// 当请求开始时,currentRunning 加 1
this.currentRunning++;
sendRequest(url).then(resolve).catch(reject).finally(() => {
// 请求结束后,currentRunning 减 1,并尝试执行下一个请求
this.currentRunning--;
this.dequeue();
});
};
this.queue.push(task);
this.dequeue(); // 每次添加请求后尝试执行请求
});
}
dequeue() {
// 如果当前运行的请求小于最大并发数,并且队列中有待执行的请求
if (this.currentRunning < this.maxConcurrent && this.queue.length) {
// 从队列中取出一个请求并执行
const task = this.queue.shift();
task();
}
}
}
// 这个函数是模拟发送请求的,实际中你可能需要替换成真实的请求操作
function sendRequest(url) {
console.log(`Sending request to ${url}`);
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Response received from ${url}`);
resolve(`Result from ${url}`);
}, Math.random() * 2000); // 随机延时以模拟请求处理时间
});
}
// 使用 RequestQueue
const requestQueue = new RequestQueue(3); // 假设我们限制最大并发数为3
// 模拟批量请求
const urls = ['url1', 'url2', 'url3', 'url4', 'url5', 'url6'];
urls.forEach(url => {
requestQueue.enqueue(url).then(result => {
console.log(result);
});
});
7. 函数科里化
函数科里化的大名应该不用做过多解释了,其特点主要就是延迟调用。
JavaScript
// 使用示例
function sum(a, b, c) {
return a + b + c;
}
// 将 sum 函数柯里化
const curriedSum = curry(sum);
// 现在可以这样使用柯里化的函数
const addOneAndTwo = curriedSum(1);
const addThree = addOneAndTwo(2);
const result = addThree(3); // 结果为 6
console.log('result',result);
核心思路:创建一个工具函数(curry),不断收集传入进来的参数。当传入进来的参数与目标函数(sum)参数个数相等时,则执行目前函数。
JavaScript
function curry(fn) {
// 返回一个函数,这个函数接受一个参数
return function curried(...args) {
// 如果当前函数已经收集了足够的参数,就调用原函数
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
// 否则,返回一个新的函数,这个函数会接受下一个参数
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
8. 选择排序
核心思路:选择排序比较简单,注意是用后面的数和每一轮的第一个数相比较,每一轮的第一个数会跟着下标的移动而移动。
JavaScript
function selectSort(nums){
// 冒泡排序是左右两个数相比较
for(let i = 0 ; i < nums.length; i++){
for(let j = i + 1; j < nums.length;j++){
if(nums[j] < nums[i]){
[nums[i],nums[j]] = [nums[j],nums[i]];
}
}
}
return nums;
}
console.log(selectSort([2,8,6,4,5,7,1]))
9. 冒泡排序
核心思路:冒泡排序的每轮过程是左右两个数相比较,比较的过程中会随着下标的移动固定住末尾的数。
JavaScript
function bubbleSort(nums){
for(let i = 0; i < nums.length; i++){
for(let j = 0; j < nums.length - i -1 ; j++){
if(nums[j+1] < nums[j]){
[nums[j],nums[j+1]] = [nums[j+1],nums[j]]
}
}
}
return nums;
}
console.log(bubbleSort([2,8,6,4,5,7,1]))
10. 快速排序
可以查下快速排序的过程是什么样子的(最好是动画的比较具象一些),这里大致描述下过程。
核心思路 :在每一轮排序的过程中找到一个中间数作为基准,将数组分割成两边,小于这个基准的划分到左边为新数组,大于这个基准的划分到右边也为新数组。然后将左右两边被分割出来的新数组递归这个过程,注意递归要有出口,出口即是仅为一个数组元素无人可以比较时。
快速排序比起选择排序和冒泡排序来说要相对复杂一些。
JavaScript
function quickSort(nums){
if(nums.length <=1){
return nums;
}
let left = 0 , right = nums.length -1;
let mid = (right + left) / 2;
let leftArr = [], rightArr = [];
const privot = nums.splice(mid,1)[0];
for(let i = 0 ; i < nums.length; i++){
if(nums[i] > privot){
rightArr.push(nums[i])
}else{
leftArr.push(nums[i])
}
}
return [...quickSort(leftArr),privot,...quickSort(rightArr)];
}
console.log(quickSort([2,8,6,4,5,7,1]))
11. 节流函数
节流函数也是前端手写的经典题目了,一般会有两种写法,这里只介绍根据时间来判断的写法。
核心思路:记录下上一次实际触发函数的时机,如果下次触发的函数时机超出了delay则执行,否则则不执行
JavaScript
function throttle(fn, delay) {
let curDate = 0;
return function (...args) {
const context = this;
if (Date.now() - curDate > delay) {
curDate = Date.now();
fn.apply(this, context);
}
};
}
function testThrottle() {
let count = 0;
const throttledFunction = throttle(function () {
count++;
console.log("count", count);
}, 1000);
for (let i = 0; i < 10; i++) {
throttledFunction();
}
setTimeout(() => {
console.log("Count after 3000ms:", count); // 应该接近1,因为1秒内只执行一次
}, 3000);
}
12. 防抖函数
防抖函数也是个经典手写题目,下面提供的是一个可以立即执行的防抖函数。
核心思路:防抖与节流最大的区别在同一段时间触发时,防抖会以最后一次作为结果,节流则是会以第一次作为结果。
JavaScript
function debounce(fn, wait, immediate = false){
let timer = null;
return function(...args){
let that = this;
if(immediate && !timer){
fn.apply(that,args)
}else if(timer){
clearTimeout(timer);
timer = null;
timer = setTimeout(()=>{
fn.apply(that,args)
},wait)
}else{
timer = setTimeout(()=>{
fn.apply(that,args)
},wait)
}
}
}
function testDebounceWithConsecutiveCalls() {
let count = 0;
const increment = () => count++;
const debouncedIncrement = debounce(increment, 500);
console.log('Test 2 start: count should be 0', count); // 初始值应为0
// 在防抖时间内连续调用
debouncedIncrement();
setTimeout(debouncedIncrement, 300);
setTimeout(debouncedIncrement, 600);
// 等待足够长的时间以确保所有调用都被处理
setTimeout(() => {
console.log('Test 2 after wait: count should be 1', count); // 等待后应为1
}, 1500);
}
testDebounceWithConsecutiveCalls();
13. 事件发布订阅
核心思路:需要实现一个函数负责订阅(on),监听函数负责收集回调函数。一个函数负责发布(emit),发布的过程即是取出该事件相关的订阅函数执行。
JavaScript
class EventEmitter {
constructor() {
this.listeners = {};
}
on(type, callback) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(callback);
}
emit(type, ...args) {
if (this.listeners[type]) {
this.listeners[type].forEach(callback => {
callback(...args);
});
}
}
}
// 使用示例
const emitter = new EventEmitter();
// 注册事件监听器
emitter.on('event', (...args) => {
console.log(...args);
});
// 触发事件
emitter.emit('event', ['Hello', 'World']);
14. 手写Promise
这个应该是很多人的噩梦了,而且我也觉得这个题目不是很必要,惯例给出一版网上的代码,具体思路看注释吧。
JavaScript
class MyPromise {
constructor(executor) {
this.status = "pending";
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === "pending") {
this.status = "fulfilled";
this.value = value;
this.onResolvedCallbacks.forEach((callback) => callback());
}
};
const reject = (reason) => {
if (this.status === "pending") {
this.status = "rejected";
this.reason = reason;
this.onRejectedCallbacks.forEach((callback) => callback());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected = typeof onRejected === "function" ? onRejected : (reason) => {
throw reason;
};
const promise2 = new MyPromise((resolve, reject) => {
if (this.status === "fulfilled") {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === "rejected") {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
if (this.status === "pending") {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
return promise2;
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach((promise) => {
promise.then(resolve, reject);
});
});
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let resolvedCount = 0;
promises.forEach((promise, index) => {
promise.then((value) => {
results[index] = value;
resolvedCount++;
if (resolvedCount === promises.length) {
resolve(results);
}
}, reject);
});
});
}
}
// resolvePromise函数,用于处理then方法中的返回值
function resolvePromise(promise2, x, resolve, reject) {
// 如果promise2和x相等,则抛出循环引用的错误
if (promise2 === x) {
return reject(new TypeError("Chaining cycle detected for promise"));
}
let called;
// 如果x不是null且x是一个对象或函数
if (x != null && (typeof x === "object" || typeof x === "function")) {
try {
// 获取x的then属性
let then = x.then;
// 如果then是一个函数
if (typeof then === "function") {
// 调用then函数,并传入x作为this,onFulfilled和onRejected作为参数
then.call(
x,
(y) => {
// 如果called为true,则直接返回
if (called) return;
called = true;
// 递归调用resolvePromise函数,处理y的值
resolvePromise(promise2, y, resolve, reject);
},
(r) => {
// 如果called为true,则直接返回
if (called) return;
called = true;
// 如果onFulfilled函数抛出异常,则调用reject函数
reject(r);
}
);
} else {
// 如果then不是一个函数,则直接resolve(x)
resolve(x);
}
} catch (error) {
// 如果called为true,则直接返回
if (called) return;
called = true;
// 如果onFulfilled函数抛出异常,则调用reject函数
reject(error);
}
} else {
// 如果x不是对象或函数,则直接resolve(x)
resolve(x);
}
}
// 测试用例
const promise1 = new MyPromise((resolve) => {
setTimeout(() => {
resolve("promise1");
}, 1000);
});
const promise2 = new MyPromise((resolve) => {
setTimeout(() => {
resolve("promise2");
}, 2000);
});
const promise3 = new MyPromise((resolve) => {
setTimeout(() => {
resolve("promise3");
}, 3000);
});
MyPromise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // ["promise1", "promise2", "promise3"]
});
MyPromise.race([promise1, promise2, promise3]).then((value) => {
console.log(value); // "promise1"
});