前端算法题和后端算法的区别:

前端更注重浏览器性能(如避免长耗时递归)。
数据量通常较小,优先考虑代码可读性。
常结合 DOM / 组件操作(如树形结构遍历对应 DOM 节点操作)

前端处理算法题

节流和防抖

  • 节流(Throttle):
    让一个函数在规定的时间间隔内,最多只执行一次。以第一次调用为准,忽略短时间内后续的调用,直到时间间隔结束。
    特点:它的作用是让一个函数在规定的时间间隔内只执行一次,适用于需要持续响应但频率受限的场景。
  • 防抖(Debounce):
    当事件在一定时间间隔内多次触发时,以最后一次调用为准,只有最后一次触发后,经过设定的延迟时间,函数才会执行。
    特点:确保函数在用户操作结束后一段时间执行,适用于需要等待用户输入稳定后再处理的场景。

函数防抖

防抖函数用于限制函数的执行频率;当事件在一定时间间隔内多次触发时,只有最后一次触发后,经过设定的延迟时间,才会执行回调函数。
常用于搜索框输入、窗口大小调整场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function debounce(fn, delay) {
// 存储定时器ID
let timer = null;
// 返回闭包函数
//args把所有传入函数的参数收集到一个数组中
return function(...args) {
clearTimeout(timer);
// 设置新定时器,利用setTimeout延迟执行
timer = setTimeout(() => {
//使用 apply 可以直接传入这个数组,不需要展开
//func.apply(this, args) 用于调用 func,并传递正确的上下文 (this) 和参数 (args)
fn.apply(this, args);
}, delay);
};
}

测试函数:

1
2
3
4
5
6
7
8
9
10
11
function search(query) {
console.log(`搜索: ${query}`);
}

// 创建防抖版本,结果输出为:搜索: abc
const debouncedSearch = debounce(search, 300);

// 模拟连续输入
debouncedSearch("a");
debouncedSearch("ab");
debouncedSearch("abc");

代码两种写法的对比

看了多篇相关文章防抖函数this部分有两种写法,但是更推荐写法1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 写法一:直接使用箭头函数的 this
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

// 写法二:显式保存 this 到变量
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
let content = this;
timer = setTimeout(() => {
fn.apply(content, args);
}, delay);
};
}

写法一:使用箭头函数的 this(继承自外层作用域);支持动态绑定 this,适用场景如事件处理
写法二:显式保存 this 到 content 变量,不支持动态绑定,适用于静态方法

为什么写法一更推荐

  • 箭头函数的 this 继承机制:
    箭头函数没有自己的 this,它继承自外层函数(即 debounce 返回的箭头函数)
    调用时动态绑定 this:当调用 onSearch.call(obj, “query”) 时,this 会指向 obj,从而在 fn.apply(this, args) 中正确传递上下文
  • 灵活性:支持动态绑定:
    调用防抖函数时可以传入不同的 this,适用于事件处理、对象方法等场景
  • 写法二:固定 this:content = this 会在 debounce 定义时绑定 this,后续调用无法动态修改

防抖函数中,为什么选择使用箭头函数而不是普通函数?

箭头函数没有自己的 this,它会继承外层作用域的 this。在防抖函数中,我们希望调用者能够动态绑定 this(例如事件处理函数的 this 指向 DOM 元素),而箭头函数能保证 this 的一致性,避免在 setTimeout 中丢失上下文。

函数节流

是一种控制函数执行频率的技术。它的作用是让一个函数在规定的时间间隔内最多只执行一次。
在一定时间间隔内,如果一个事件被多次触发,只以第一次被触发为准,忽略短时间内后续的调用,直到时间间隔结束。c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function throttle(fn, delay) {
// 上次执行时间
let lastTime = 0;

return function(...args) {
// 获取当前时间
const now = Date.now();

// 计算时间差
const diff = now - lastTime;

// 如果时间差大于等于延迟时间,执行函数
if (diff >= delay) {
// 更新上次执行时间
lastTime = now;
// 执行传入的函数,并传递参数
fn.apply(this, args);
}
};
}

axios/fetch

post请求

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
fetch("https://api.example.com/data", {
method: "POST",
headers: {
"Content-Type": "application/json"
'Authorization': 'Bearer your_token'
},
body: JSON.stringify({
username: 'Alice',
password: '123456'
})
})
.then(response => {
// 同时处理成功和失败的响应
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error(error));
//axios
axios.post('https://api.example.com/submit', {
username: 'Alice',
password: '123456'
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your_token'
}
})
.then(response => console.log(response.data))
.catch(error => console.error(error));
  • 多个 .then:用于处理异步操作的结果,每个 .then 处理前一个步骤的结果。
  • response 和 data:来自 Fetch API 的响应对象和解析后的数据。
  • return:返回一个值或 Promise,传递给下一个 .then 进行处理

axios创建单独的实例

1
2
3
4
5
6
7
//创建单独的实例发送网络请求
const instance1 = axios.create({
baseURL = "https//xxxxxx",
timeout = 5000,
headers={}
})

拦截器

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
import axios from 'axios';

// 创建实例
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000
});

// 请求拦截器
instance.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
//加到请求头(config.headers)中的 Authorization 字段里
config.headers.Authorization = `Bearer ${token}`;
}
return config;// 必须返回 config,否则请求无法发送
},
error => {
return Promise.reject(error);// 抛出错误
}
);

// 响应拦截器
instance.interceptors.response.use(
response => {
// 响应成功,统一处理响应数据
if (response.status === 200) {
return response.data;
} else {
return Promise.reject(new Error(`HTTP 错误: ${response.status}`));
}
},
error => {
if (error.response) {
// 服务器返回了错误响应(如 401、404、500)
console.error('服务器错误:', error.response.status, error.response.data);
} else if (error.request) {
// 请求已发出,但未收到响应(如网络断开)
console.error('请求无响应:', error.request);
} else {
// 其他错误(如请求配置错误)
console.error('请求异常:', error.message);
}
return Promise.reject(error);
}
);

export default instance;

promise,async/await

创建 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ?
resolve('Success!') :
reject(new Error('Failed!'));
}, 1000);
});

// 1.2 处理结果
promise
.then(result => console.log(result)) // 成功输出 "Success!"
.catch(error => console.error(error)) // 失败捕获错误
.finally(() => console.log('Done')); // 无论成败都会执行

基础 fetch 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function fetchData(){
try{
const response = await fetchData('url')
const data = await response.json()
console.log(data)
}catch(error){
console.log(error)
}
}

async function fetchData(url, content){
try{
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify({content}),
})
}catch(error){
console.log(error)
}
}

基础axios请求

1
2
3
4
5
6
7
8
9
10
11
12
import axios from 'axios'

async function fetchData(){
try{
const response = async axios.get('url')
console.log(response.data)
}catconsole.log(error)ch(error){

}

}

串行请求,并行请求,分批次处理请求

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
// 串行请求:依次等待每个请求完成
async function sequentialRequests() {
const user1 = await fetchUser(1);
const user2 = await fetchUser(2);
const user3 = await fetchUser(3);
return [user1, user2, user3];
}

// 并发请求:同时发送多个请求(性能更优)
async function parallelRequests() {
const promises = [fetchUser(1), fetchUser(2), fetchUser(3)];
return await Promise.all(promises);
}
//并发请求
async function getData(){
const [data1,data2] = await Promise.all([
axios.get('/api/data1'),
axios.get('/api/data2')
]);
return { user: user.data, orders: orders.data };
}


// 限制并发数量(重要场景:避免请求过多导致性能问题)
async function limitConcurrentRequests(ids, limit = 3) {
const results = [];

// 分批次处理请求
for (let i = 0; i < ids.length; i += limit) {
const batch = ids.slice(i, i + limit);
const batchResults = await Promise.all(batch.map(id => fetchUser(id)));
results.push(...batchResults);
}

return results;
}

数据结构算法题

两数之和 (LeetCode 1)(数组 + 哈希表)

前端应用场景:表单搜索联想、数据匹配校验等

给定一个整数数组 nums 和一个目标值 target,在数组中找出两个数,使其和为 target,返回它们的索引(假设只有唯一解,不能重复使用同一个元素)
输入:nums = [2, 7, 11, 15], target = 9
输出:[0, 1](因为 2 + 7 = 9)

  1. 暴力解法(量少行,不推荐)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let nums = [3, 7, 12, 14]
    let target = 15
    function twoSum1(nums, target) {
    const n = nums.length;
    for (let i = 0; i < n; i++) { // 外层循环:选第一个数
    for (let j = i + 1; j < n; j++) { // 内层循环:选第二个数(j在i后面)
    if (nums[i] + nums[j] === target) { // 检查和是否等于目标数
    return [i, j]; // 找到就返回索引
    }
    }
    }
    return [];
    }

    console.log("原来数组:",nums,"目标值:",target,"twoSum1返回索引:",twoSum1(nums,target))

  2. 使用哈希表(推荐)
  • 当遍历到一个数 nums[i] 时,需要知道是否存在另一个数 complement = target - nums[i],如果存在,那这两个数就是答案。
  • 用 Map 来记录每个数出现的位置,这样每次遇到 nums[i] 时,只需在 Map 中查 complement 是否存在,存在就直接返回结果
  • Map 存的是 “之前出现过的数及其索引”,每次新来一个数,先查补数是否在 Map 里,再把自己存进去。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    let nums = [3, 7, 12, 14]
    let target = 15
    function twoSum2(nums, target) {
    const map = new Map(); // 创建一个Map,用来存“数值:索引”
    for (let i = 0; i < nums.length; i++) { // 遍历数组,i是当前数的索引
    const currentNum = nums[i]; // 当前数
    const complement = target - currentNum; // 计算需要的补数

    // 重点:先查补数是否在Map里
    if (map.has(complement)) {
    // 如果存在,说明前面已经有一个数等于complement,直接返回它的索引和当前索引
    return [map.get(complement), i];
    }

    // 如果不存在,就把当前数和索引存入Map,供后面的数查询
    map.set(currentNum, i);
    }
    return []; // 题目保证有解,这行可以省略
    console.log("原来数组:",nums,"目标值:",target,"twoSum2返回索引:",twoSum2(nums,target))

    }

回文数(LeetCode 9)

判断一个整数是否是回文数。不允许将整数转为字符串

输入:121 → 输出:true(正序和逆序都是 121)
输入:-121 → 输出:false(负数一定不是回文数)
输入:10 → 输出:false(逆序为 01,即 1,与原数不相等)

  1. 暴力解法(不推荐,但必须理解):
    反转整个数:将原数 x 逐位反转,得到反转后的数 reversed。
    比较:判断 x 是否等于 reversed

    取最后一位:original % 10
    移除最后一位:Math.floor(original / 10)
    构建反转数:reversed = reversed * 10 + 最后一位

为什么用 Math.floor()?
js中整数除法会返回浮点数(如 123 / 10 = 12.3),而我们需要的是整数部分12,Math.floor() 向下取整,确保结果为整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function isPalindrome(x) {
if (x < 0) return false;
let original = x; // 保存原数,避免修改x(后续需用x进行比较)
let reversed = 0;
// 只要原数还有剩余位数(即不为0),就继续反转
while (original !== 0) {
// 1. 取原数的最后一位
const lastDigit = original % 10;
// 2. 将最后一位添加到反转数的末尾
reversed = reversed * 10 + lastDigit;
// 3. 移除原数的最后一位(相当于原数右移一位)
original = Math.floor(original / 10);
// 打印中间过程(仅用于理解,实际代码无需此行)
console.log(`当前反转数: ${reversed}, 剩余原数: ${original}`);
}

// 比较原数和反转后的数是否相等
return x === reversed;
}

// 测试示例:123 → 反转后为321 → 不相等 → 返回false
console.log(isPalindrome(123)); // 输出false
  1. 最优解法:反转一半数字
    回文数的后半部分反转后应与前半部分相同;用x判断是否反转到一半,当原数x小于等于反转后的数reverted时,说明已反转一半

    以 x = 1221 为例:
    初始状态:x = 1221,reverted = 0

    1. 逐位反转:
      取最后一位 1 → reverted = 0 * 10 + 1 = 1,x = 122
      取最后一位 2 → reverted = 1 * 10 + 2 = 12,x = 12
    2. 停止条件:
      此时 x = 12 ≤ reverted = 12,说明已反转一半,停止循环
    3. 比较结果:
      x = 12 与 reverted = 12 相等 → 返回 true

如果是奇数长度的回文数(如12321)
反转过程:x = 12321 → reverted = 1 → 12 → 123
停止时:x = 12,reverted = 123
处理方式:去除中间位(reverted / 10),再比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function isPalindrome(x) {
// 特殊情况处理:
// 1. 负数不可能是回文数(如 -121)
// 2. 以0结尾的非零数(如10、120)不可能是回文数(反转后会丢失前导0)
if (x < 0 || (x % 10 === 0 && x !== 0)) {
return false;
}

let reverted = 0;
while (x > reverted) {
reverted = reverted * 10 + x % 10; // 取x的最后一位,添加到reverted
x = Math.floor(x / 10); // 移除x的最后一位
}

// 当数字长度为偶数时(如1221),x和reverted相等
// 当数字长度为奇数时(如12321),reverted会多一位中间数(如123),需去掉中间数再比较
return x === reverted || x === Math.floor(reverted / 10);
}

反转字符串(LeetCode 344)

编写一个函数,反转输入的字符串(原地修改,不使用额外数组)。
示例:输入 s = [“h”,”e”,”l”,”l”,”o”],输出 [“o”,”l”,”l”,”e”,”h”]

核心思路:双指针对撞法
定义两个指针:
left 指向数组开头(0),right 指向数组末尾(length-1)。
交换元素并移动指针:
交换 s[left] 和 s[right],然后 left++,right–。
直到 left >= right 时停止(此时所有元素已反转)

1
2
3
4
5
6
7
8
9
10
11
12
function reverseString(s) {
let left = 0; // 左指针,从数组开头开始
let right = s.length - 1; // 右指针,从数组末尾开始

while (left < right) { // 当左指针在右指针左侧时,继续交换
// 交换左右指针指向的元素
[s[left], s[right]] = [s[right], s[left]];
left++; // 左指针右移一位
right--; // 右指针左移一位
}
// 无需返回值,原数组已被修改
}