目录表
一、综合
1、前端性能优化
前端性能优化的方式包括:
- 减少HTTP请求:合并文件(JS、CSS、图片),使用雪碧图。
- 资源压缩和代码优化:压缩JS、CSS文件,使用Tree Shaking去除未用代码。
- 异步加载资源:懒加载图片,异步加载JS和CSS。
- 缓存优化:合理使用浏览器缓存和Service Worker。
- 图片优化:选择合适格式和压缩图片(如WebP)。
- 减少DOM操作:优化DOM更新,减少重排和重绘。
- 使用CDN:将静态资源放在CDN上,减少加载时间。
这些方法结合使用能显著提升页面加载速度和性能。
2、Promise
Promise
是 JavaScript 中用于处理异步操作的对象。它代表一个尚未完成但预期将来会完成的操作,可以处于以下三种状态之一:
- Pending(待定):初始状态,表示操作尚未完成。
- Fulfilled(已完成):操作成功完成,返回一个值。
- Rejected(已拒绝):操作失败,返回一个错误。
通过 .then()
和 .catch()
方法,可以处理成功和失败的回调,Promise
使得异步代码更加易于理解和维护。
二、手写代码
1、数组去重
以下是几种实现数组去重函数的方法:
方法 1:使用 Set
(推荐)
代码:
function uniqueArray(arr) {
return [...new Set(arr)];
}
// 测试
console.log(uniqueArray([1, 2, 2, 3, 4, 4])); // [1, 2, 3, 4]
解析:
Set
是一种数据结构,能自动去除重复值。- 使用扩展运算符将
Set
转换为数组。
方法 2:使用 filter
和 indexOf
代码:
function uniqueArray(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
// 测试
console.log(uniqueArray([1, 2, 2, 3, 4, 4])); // [1, 2, 3, 4]
解析:
filter
遍历数组,保留第一个出现的元素,过滤掉后续重复项。
方法 3:使用 for
循环
代码:
function uniqueArray(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (!result.includes(arr[i])) {
result.push(arr[i]);
}
}
return result;
}
// 测试
console.log(uniqueArray([1, 2, 2, 3, 4, 4])); // [1, 2, 3, 4]
解析:
- 遍历原数组,通过
includes
检查结果数组中是否已存在当前元素。
根据场景选择最合适的方法即可!
2、冒泡排序
以下是使用 JavaScript 手写的冒泡排序:
function bubbleSort(arr) {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
// 提前退出标志位
let swapped = false;
for (let j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换相邻元素
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swapped = true;
}
}
// 如果本轮没有交换过,数组已排序好,提前退出
if (!swapped) break;
}
return arr;
}
// 测试
const array = [5, 2, 9, 1, 5, 6];
console.log('排序前:', array);
console.log('排序后:', bubbleSort(array));
算法说明
- 冒泡排序通过多次遍历数组,每次比较相邻两个元素,如果顺序不对就交换。
- 每一轮遍历结束后,最大的元素会移动到数组的末尾。
- 使用
swapped
标志位优化:若某轮遍历没有发生交换,说明数组已排序好,可以提前退出。
时间复杂度
- 最好情况:O(n)(数组本身有序)。
- 最坏情况:O(n²)(数组完全逆序)。
- 平均情况:O(n²)。
3、快速排序
以下是使用 JavaScript 手写的快速排序算法:
function quickSort(arr) {
// 如果数组长度为 0 或 1,直接返回
if (arr.length <= 1) {
return arr;
}
// 选择基准元素,一般选择中间值
const pivot = arr[Math.floor(arr.length / 2)];
const left = [];
const right = [];
const equal = [];
// 遍历数组,将元素按大小分到不同的数组
for (const num of arr) {
if (num < pivot) {
left.push(num);
} else if (num > pivot) {
right.push(num);
} else {
equal.push(num);
}
}
// 递归排序左右两部分,并合并结果
return [...quickSort(left), ...equal, ...quickSort(right)];
}
// 测试
const array = [5, 2, 9, 1, 5, 6];
console.log('排序前:', array);
console.log('排序后:', quickSort(array));
算法说明
- 选择基准值(Pivot): 通常从数组中选择一个元素作为基准,这里选择数组的中间元素。
- 划分子数组: 将数组中的元素根据与基准的大小分为三部分:
- 比基准小的元素放入
left
。 - 与基准相等的元素放入
equal
。 - 比基准大的元素放入
right
。
- 比基准小的元素放入
- 递归处理: 对
left
和right
子数组递归调用快速排序。 - 合并结果: 最后将
left
、equal
和right
合并成一个已排序的数组。
4、浅拷贝和深拷贝
浅拷贝只复制对象的第一层属性,嵌套对象仅复制引用,而深拷贝会递归复制对象及其嵌套对象的所有层级,创建完全独立的副本。
浅拷贝方法
- 使用
Object.assign()
const shallowCopy = (obj) => Object.assign({}, obj);
- 使用展开运算符
const shallowCopy = (obj) => ({ ...obj });
深拷贝方法
- 使用
JSON.parse()
和JSON.stringify()
(适用于无函数和特殊对象的场景)
const deepCopy = (obj) => JSON.parse(JSON.stringify(obj));
- 递归实现深拷贝
// 定义 deepCopy 函数,接收一个对象作为参数
const deepCopy = (obj) => {
// 如果对象为 null 或者不是对象类型,直接返回该对象
if (obj === null || typeof obj!== 'object') return obj;
// 判断对象是否为数组,如果是数组,创建一个空数组作为复制对象,否则创建一个空对象
const copy = Array.isArray(obj)? [] : {};
// 遍历对象的键
for (const key in obj) {
// 确保只复制对象自身的属性,不复制原型链上的属性
if (obj.hasOwnProperty(key)) {
// 递归调用 deepCopy 函数,对每个属性进行深拷贝
copy[key] = deepCopy(obj[key]);
}
}
// 返回深拷贝后的对象
return copy;
};
- 使用
structuredClone
(现代浏览器支持)
const deepCopy = (obj) => structuredClone(obj);
总结
- 浅拷贝适合拷贝简单对象。
- 深拷贝应根据场景选择合适方法:如递归更灵活,
JSON
方法简单但有限制,structuredClone
是现代浏览器的首选。
5、数组扁平化
你可以使用递归来实现一个数组扁平化函数,将多维数组变成一维数组。下面是一个JavaScript函数示例:
function flattenArray(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
// 如果当前元素是数组,递归调用flattenArray并合并结果
result = result.concat(flattenArray(arr[i]));
} else {
// 如果不是数组,直接将元素添加到结果数组中
result.push(arr[i]);
}
}
return result;
}
// 示例用法
const nestedArray = [1, [2, [3, 4], 5], 6];
const flattenedArray = flattenArray(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]
这个flattenArray
函数接受一个多维数组作为参数,然后使用递归遍历数组的每个元素。如果当前元素是数组,它会递归调用flattenArray
来处理内部数组,并将结果合并到结果数组中。如果当前元素不是数组,它将直接将元素添加到结果数组中。最终,返回一个一维数组,其中包含了多维数组中的所有元素。
6、防抖和节流
以下是手写的防抖和节流函数:
防抖函数
防抖函数在事件触发后,只有在指定的时间间隔内没有再次触发,才会执行函数。
function debounce(func, delay) {
let timer; // 定时器
return function (...args) {
clearTimeout(timer); // 清除上一次的定时器
timer = setTimeout(() => {
func.apply(this, args); // 保证 `this` 的正确性
}, delay);
};
}
// 测试防抖
const log = debounce(() => console.log('防抖触发!'), 1000);
window.addEventListener('resize', log);
节流函数
节流函数在指定的时间间隔内,只允许执行一次函数。
使用定时器
function throttle(func, delay) {
let timer;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args); // 执行函数
timer = null; // 清空定时器
}, delay);
}
};
}
// 测试节流
const log = throttle(() => console.log('节流触发!'), 1000);
window.addEventListener('scroll', log);
对比与选择
- 防抖适用于短时间内频繁触发的场景,比如搜索框输入、窗口大小调整等。
- 节流适用于需要控制频率的场景,比如滚动事件、页面点击等。
7、JS对象转为树形结构
将一个扁平的 JavaScript 对象数组转化为树形结构,可以按照以下思路实现:
确定数据结构
- 每个对象有一个唯一标识
id
。 - 每个对象有一个父级标识
parentId
,顶级节点的parentId
为null
或其他特殊值。
- 每个对象有一个唯一标识
实现步骤
- 创建一个哈希表以便快速查找。
- 遍历数据,将对象挂载到其父节点的
children
数组中。
以下是具体实现代码:
function buildTree(data) {
const tree = [];
const map = new Map();
// 初始化哈希表,用 id 作为键
data.forEach(item => {
map.set(item.id, { ...item, children: [] });
});
// 构建树
data.forEach(item => {
const node = map.get(item.id);
if (item.parentId === null) {
// 顶级节点
tree.push(node);
} else {
// 非顶级节点,加入父节点的 children
const parent = map.get(item.parentId);
if (parent) {
parent.children.push(node);
}
}
});
return tree;
}
// 测试数据
const input = [
{ id: 1, name: '根节点', parentId: null },
{ id: 2, name: '子节点 1', parentId: 1 },
{ id: 3, name: '子节点 2', parentId: 1 },
{ id: 4, name: '子节点 1-1', parentId: 2 },
{ id: 5, name: '子节点 2-1', parentId: 3 },
];
const tree = buildTree(input);
console.log(JSON.stringify(tree, null, 2));
输出结果
[
{
"id": 1,
"name": "根节点",
"parentId": null,
"children": [
{
"id": 2,
"name": "子节点 1",
"parentId": 1,
"children": [
{
"id": 4,
"name": "子节点 1-1",
"parentId": 2,
"children": []
}
]
},
{
"id": 3,
"name": "子节点 2",
"parentId": 1,
"children": [
{
"id": 5,
"name": "子节点 2-1",
"parentId": 3,
"children": []
}
]
}
]
}
]
说明
时间复杂度:
- 哈希表初始化:O(n)
- 遍历数据构建树:O(n)
总体复杂度为 O(n)。
特点:
children
数组在每个节点中表示其子节点。- 顶级节点存储在
tree
数组中。
这样可以轻松构建树形结构,适用于菜单、目录等场景。
8、提取 URL 参数
以下是提取 URL 中的 GET 查询参数并将其存储为 JSON 结构的实现代码:
function parseQueryParams(url) {
const queryString = url.split('?')[1]; // 获取问号后面的部分
if (!queryString) {
return {}; // 如果没有查询参数,返回空对象
}
const params = {};
const pairs = queryString.split('&'); // 根据 & 拆分每个键值对
pairs.forEach(pair => {
const [key, value] = pair.split('='); // 根据 = 拆分键和值
params[decodeURIComponent(key)] = decodeURIComponent(value || ''); // 解码并处理空值
});
return params;
}
// 测试
const url = 'https://example.com/page?name=John&age=30&city=New%20York';
const result = parseQueryParams(url);
console.log(result);
输出结果
{
"name": "John",
"age": "30",
"city": "New York"
}
代码说明
分割 URL 和查询字符串
使用split('?')
提取 URL 中?
后面的部分。拆分键值对
使用split('&')
将查询字符串按&
拆分为每个键值对。解析键和值
使用split('=')
将每个键值对拆分成键和值,同时使用decodeURIComponent
解码 URL 编码的字符。处理空值
如果键没有对应的值,默认值为''
。
9、交换a,b的值
以下是几种方法在不使用临时变量的情况下交换两个变量 a
和 b
的值:
1. 使用数组解构
let a = 5, b = 3;
[a, b] = [b, a];
console.log(a, b); // 输出:3, 5
2. 使用加减法
let a = 5, b = 3;
a = a + b; // a = 8
b = a - b; // b = 5
a = a - b; // a = 3
console.log(a, b); // 输出:3, 5
3. 使用异或(XOR)运算
let a = 5, b = 3;
a = a ^ b; // a = 6 (0101 ^ 0011 = 0110)
b = a ^ b; // b = 5 (0110 ^ 0011 = 0101)
a = a ^ b; // a = 3 (0110 ^ 0101 = 0011)
console.log(a, b); // 输出:3, 5
推荐方法
- 使用数组解构 是最直观且安全的现代方法。
- 加减法 和 异或 适合对性能要求高的场景,但需注意溢出问题。
10、1到100求和
以下是使用 JavaScript 实现 1 到 100 求和的代码:
// 方法一:使用循环
let sum = 0;
for (let i = 1; i <= 100; i++) {
sum += i;
}
console.log("1到100的和(循环):", sum);
// 方法二:使用递归求和
function sumRecursive(n) {
if (n === 1) {
return 1; // 递归终止条件
}
return n + sumRecursive(n - 1); // 递归调用
}
// 测试递归方法
const result = sumRecursive(100);
console.log("1到100的和(递归):", result);
输出结果:
5050