常见算法 - JavaScript 特性
判断循环引用
思路:通过深度优先搜索 (DFS) 遍历对象,并使用一个 parentArr 数组(或 WeakSet)来记录已经访问过的父级对象。如果在遍历过程中发现当前对象与 parentArr 中的某个对象相同,则说明存在循环引用。
/**
* 判断对象是否存在循环引用
* @param {object} obj 要检测的对象
* @param {Array<object>} [parentArr] 存储已访问的父级对象,用于检测循环引用
* @returns {boolean} 如果存在循环引用则返回 true,否则返回 false
*/
const isCycleObject = (obj, parentArr = []) => {
// 如果不是对象或者为 null,则不可能存在循环引用
if (obj === null || typeof obj !== 'object') {
return false;
}
// 检查当前对象是否已经在父级链中
for (const pObj of parentArr) {
if (pObj === obj) {
return true; // 发现循环引用
}
}
// 将当前对象添加到父级链中
const newParentArr = [...parentArr, obj];
// 遍历对象的属性
for (const key in obj) {
// 确保只处理对象自身的属性,不处理原型链上的属性
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// 如果属性值是对象,则递归检测
if (typeof obj[key] === 'object' && isCycleObject(obj[key], newParentArr)) {
return true; // 子对象存在循环引用
}
}
}
return false; // 未发现循环引用
};
// 示例
const a = 1;
const b = { a };
const c = { b };
const o = { d: { a: 3 }, c };
o.c.b.aa = a; // 正常赋值
// 创建循环引用
const cycleObj = {};
cycleObj.self = cycleObj;
console.log('存在循环引用:', isCycleObject(cycleObj)); // true
const objWithCycle = {
prop1: 1,
prop2: {}
};
objWithCycle.prop2.parent = objWithCycle;
console.log('存在循环引用:', isCycleObject(objWithCycle)); // true
const normalObj = {
prop1: 1,
prop2: {
prop3: 3
}
};
console.log('不存在循环引用:', isCycleObject(normalObj)); // false发布订阅模式 (Publish-Subscribe Pattern)
发布订阅模式是一种松耦合的通信机制,其中发布者和订阅者之间没有直接的依赖关系。发布者发布事件,订阅者监听事件并执行相应的处理函数。
/**
* 事件中心类
*/
class EventCenter {
constructor() {
this.events = {}; // 存储事件及其对应的处理函数列表
}
/**
* 订阅事件
* @param {string} topic 事件主题
* @param {Function} handler 事件处理函数
*/
subscribe(topic, handler) {
if (!this.events[topic]) {
this.events[topic] = [];
}
this.events[topic].push(handler);
}
/**
* 发布事件
* @param {string} topic 事件主题
* @param {...any} params 传递给处理函数的参数
*/
publish(topic, ...params) {
if (!this.events[topic] || this.events[topic].length === 0) {
// console.warn(`Topic "${topic}" has no subscribers.`);
return; // 没有订阅者,直接返回
}
this.events[topic].forEach(handler => {
handler(...params);
});
}
/**
* 取消订阅事件
* @param {string} topic 事件主题
* @param {Function} [handler] 要取消的特定处理函数,如果未提供则取消该主题所有订阅
*/
unsubscribe(topic, handler) {
if (!this.events[topic]) {
// console.warn(`Topic "${topic}" is not registered.`);
return;
}
if (!handler) {
delete this.events[topic]; // 取消该主题所有订阅
return;
}
const index = this.events[topic].indexOf(handler);
if (index !== -1) {
this.events[topic].splice(index, 1);
} else {
// console.warn(`Handler for topic "${topic}" not found.`);
}
if (this.events[topic].length === 0) {
delete this.events[topic]; // 如果该主题没有处理函数了,则删除该主题
}
}
}
// 示例使用
const eventBus = new EventCenter();
const handler1 = (data) => console.log('Handler 1 received:', data);
const handler2 = (data) => console.log('Handler 2 received:', data);
eventBus.subscribe('userLogin', handler1);
eventBus.subscribe('userLogin', handler2);
eventBus.subscribe('productAdded', (item) => console.log('Product added:', item));
eventBus.publish('userLogin', { userId: 123, username: 'Alice' });
eventBus.publish('productAdded', { productId: 456, name: 'Laptop' });
eventBus.unsubscribe('userLogin', handler1);
eventBus.publish('userLogin', { userId: 789, username: 'Bob' }); // 只有 handler2 会收到
eventBus.unsubscribe('userLogin'); // 取消所有 userLogin 订阅
eventBus.publish('userLogin', { userId: 101, username: 'Charlie' }); // 无人收到发布订阅模式用于处理不同系统组件之间的信息交流,这些组件可以不知道对方的存在,从而实现解耦。
观察者模式 (Observer Pattern)
观察者模式定义了对象之间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。
/**
* 观察者类
*/
class Observer {
constructor(name) {
this.name = name;
}
/**
* 接收通知并更新
* @param {*} data 通知者传递的数据
*/
update(data) {
console.log(`Observer ${this.name} received update:`, data);
}
}
/**
* 通知者(Subject)类
*/
class Notifier {
constructor() {
this.observerList = []; // 存储所有观察者
}
/**
* 添加观察者
* @param {Observer} observer 要添加的观察者实例
*/
add(observer) {
if (this.observerList.indexOf(observer) === -1) { // 确保不重复添加
this.observerList.push(observer);
console.log(`Observer ${observer.name} added.`);
} else {
console.log(`Observer ${observer.name} already exists.`);
}
}
/**
* 移除观察者
* @param {Observer} observer 要移除的观察者实例
*/
remove(observer) {
const index = this.observerList.indexOf(observer);
if (index !== -1) {
this.observerList.splice(index, 1);
console.log(`Observer ${observer.name} removed.`);
} else {
console.log(`Observer ${observer.name} not found.`);
}
}
/**
* 通知所有观察者
* @param {*} data 传递给观察者的数据
*/
notify(data) {
console.log('Notifying all observers...');
this.observerList.forEach(observer => {
observer.update(data);
});
}
}
// 示例使用
const dog = new Observer('Dog');
const cat = new Observer('Cat');
const bird = new Observer('Bird');
const weatherStation = new Notifier();
weatherStation.add(dog);
weatherStation.add(cat);
weatherStation.add(bird);
weatherStation.notify('Sunny day!'); // 通知所有观察者
weatherStation.remove(cat); // 移除 cat 观察者
weatherStation.notify('Rainy day!'); // 只有 dog 和 bird 会收到通知观察者模式下,观察者们通常会执行同一类(或相关联)的处理逻辑,但每个观察者也可以自定义其 update 方法。
大数相加
思路:模拟小学数学中的竖式加法,从个位开始逐位相加,并处理进位。
/**
* 实现两个大数相加
* @param {string} num1 第一个大数(字符串形式)
* @param {string} num2 第二个大数(字符串形式)
* @returns {string} 相加结果(字符串形式)
*/
function bigNumberAdd(num1, num2) {
let i = num1.length - 1;
let j = num2.length - 1;
let carry = 0; // 进位
let result = '';
while (i >= 0 || j >= 0 || carry !== 0) {
const digit1 = i >= 0 ? parseInt(num1[i]) : 0;
const digit2 = j >= 0 ? parseInt(num2[j]) : 0;
const sum = digit1 + digit2 + carry;
result = (sum % 10) + result; // 当前位的和
carry = Math.floor(sum / 10); // 计算进位
i--;
j--;
}
return result;
}
// 示例
console.log('大数相加:', bigNumberAdd('123', '456')); // "579"
console.log('大数相加:', bigNumberAdd('99', '1')); // "100"
console.log('大数相加:', bigNumberAdd('1234567890123456789', '9876543210987654321')); // "11111111101111111110"大数相乘
思路:模拟小学数学中的竖式乘法,将一个数的每一位与另一个数相乘,然后将结果错位相加。
/**
* 实现两个大数相乘
* @param {string} num1 第一个大数(字符串形式)
* @param {string} num2 第二个大数(字符串形式)
* @returns {string} 相乘结果(字符串形式)
*/
function multiply(num1, num2) {
// 如果其中一个数为 '0',则结果为 '0'
if (num1 === '0' || num2 === '0') {
return '0';
}
// 结果数组,长度为 num1.length + num2.length,并初始化为 0
const resultArr = new Array(num1.length + num2.length).fill(0);
// 从个位开始遍历 num2
for (let i = num2.length - 1; i >= 0; i--) {
// 从个位开始遍历 num1
for (let j = num1.length - 1; j >= 0; j--) {
// 计算当前位乘积,并加到对应的位置
// resultArr[i + j + 1] 对应当前乘积的个位
// resultArr[i + j] 对应当前乘积的十位(进位)
const product = parseInt(num2[i]) * parseInt(num1[j]);
const sum = resultArr[i + j + 1] + product;
resultArr[i + j + 1] = sum % 10; // 当前位
resultArr[i + j] += Math.floor(sum / 10); // 进位
}
}
// 处理结果数组开头的 0
let result = resultArr.join('');
// 去除前导零,如果结果是 "00123" 应该变成 "123"
// 如果结果是 "0",则保留一个 "0"
return result.replace(/^0+(?=\d)/, '');
}
// 示例
console.log('大数相乘:', multiply('123', '45')); // "5535"
console.log('大数相乘:', multiply('99', '99')); // "9801"
console.log('大数相乘:', multiply('123456789', '987654321')); // "121932631112635269"防抖 (Debounce) 与 节流 (Throttle)
防抖和节流是优化高频事件处理的两种常见技术。
防抖 (Debounce)
定义:在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。 应用场景:搜索框输入、窗口 resize、滚动事件等。
/**
* 防抖函数
* @param {Function} fn 要执行的函数
* @param {number} wait 延迟时间(毫秒)
* @returns {Function} 经过防抖处理的函数
*/
function debounce(fn, wait) {
let timer = null; // 定时器 ID
return function(...args) { // 使用 rest 参数捕获所有参数
const context = this; // 保存函数执行时的 this 上下文
// 如果定时器存在,则清除之前的定时器
if (timer) {
clearTimeout(timer);
}
// 重新设置定时器
timer = setTimeout(() => {
fn.apply(context, args); // 在延迟结束后执行函数
timer = null; // 执行后清除定时器
}, wait);
};
}
// 示例
// const myFunc = () => console.log('Function executed!');
// const debouncedFunc = debounce(myFunc, 500);
// window.addEventListener('resize', debouncedFunc);节流 (Throttle)
定义:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。 应用场景:滚动加载、高频点击、游戏射击等。
/**
* 节流函数
* @param {Function} fn 要执行的函数
* @param {number} delay 延迟时间(毫秒)
* @returns {Function} 经过节流处理的函数
*/
function throttle(fn, delay) {
let lastExecTime = 0; // 上次执行时间
return function(...args) {
const context = this;
const now = Date.now(); // 当前时间
// 如果当前时间距离上次执行时间超过了延迟时间,则执行函数
if (now - lastExecTime >= delay) {
lastExecTime = now; // 更新上次执行时间
fn.apply(context, args);
}
};
}
// 示例
// const myScrollHandler = () => console.log('Scroll handled!');
// const throttledScrollHandler = throttle(myScrollHandler, 200);
// window.addEventListener('scroll', throttledScrollHandler);Equal 函数 (深比较)
实现一个 equal 函数,用于深度比较两个 JavaScript 值是否相等,包括基本类型、对象、数组、日期、正则表达式以及处理循环引用。
/**
* 深度比较两个 JavaScript 值是否相等
* @param {*} a 第一个值
* @param {*} b 第二个值
* @param {WeakSet} [seen] 用于处理循环引用的 WeakSet
* @returns {boolean} 如果相等则返回 true,否则返回 false
*/
function equal(a, b, seen = new WeakSet()) {
// 1. 基本类型及引用地址相同的情况
if (a === b) {
// 处理特殊情况:NaN !== NaN,但应视为相等
return a !== a ? b !== b : true;
}
// 2. 其中一个为 null 或非对象类型(排除基本类型后,剩余为引用类型)
// 如果 a 或 b 是 null,或者不是对象,则不相等(因为 a !== b 已经排除了两者都为 null 的情况)
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
return false;
}
// 3. 处理循环引用
// 如果已经比较过这对对象,直接返回 true,避免死循环
if (seen.has(a) && seen.has(b)) {
return true;
}
seen.add(a);
seen.add(b);
// 4. 特殊对象处理
// 4.1 Date:比较时间戳
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
// 4.2 RegExp:比较源文本、标志位(忽略 lastIndex)
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
// 4.3 Map:比较键值对
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) return false;
for (const [key, val] of a) {
if (!b.has(key) || !equal(val, b.get(key), seen)) {
return false;
}
}
return true;
}
// 4.4 Set:比较元素
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) return false;
for (const val of a) {
if (!b.has(val) || !equal(val, Array.from(b).find(item => equal(item, val, seen)), seen)) {
// Set 比较复杂,需要找到对应的元素进行深度比较
// 这里的 find 效率不高,更优解是先将 Set 转换为排序数组再比较
return false;
}
}
return true;
}
// 5. 数组/对象:获取键并比较长度
const isArrayA = Array.isArray(a);
const isArrayB = Array.isArray(b);
if (isArrayA !== isArrayB) return false; // 一个是数组,一个不是,则不相等
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
// 6. 递归比较每个键的值
for (const key of keysA) {
// 键存在性检查
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
// 递归比较值
if (!equal(a[key], b[key], seen)) return false;
}
return true;
}
// 示例
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = { a: 1, b: { c: 3 } };
console.log('obj1 === obj2:', equal(obj1, obj2)); // true
console.log('obj1 === obj3:', equal(obj1, obj3)); // false
const arr1 = [1, { a: 2 }];
const arr2 = [1, { a: 2 }];
console.log('arr1 === arr2:', equal(arr1, arr2)); // true
// 循环引用示例
const cycleA = {};
const cycleB = {};
cycleA.b = cycleB;
cycleB.a = cycleA;
console.log('cycleA === cycleB:', equal(cycleA, cycleB)); // true深层对象过滤
实现一个 deepFilterEmpty 函数,用于深度过滤对象或数组中的空值(null 或 undefined)。
/**
* 深度过滤对象或数组中的空值(null 或 undefined)
* @param {object | Array} obj 要过滤的对象或数组
* @returns {object | Array} 过滤后的对象或数组
*/
function deepFilterEmpty(obj) {
// 若不是对象或为 null,直接返回(递归终止条件)
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 数组特殊处理:遍历每个元素并递归过滤,保留非空元素
if (Array.isArray(obj)) {
// 从后往前遍历,避免 splice 影响后续索引
for (let i = obj.length - 1; i >= 0; i--) {
const item = obj[i];
// 递归过滤元素
const filteredItem = deepFilterEmpty(item);
// 若元素过滤后为空值,从数组中移除
if (filteredItem === null || filteredItem === undefined) {
obj.splice(i, 1);
} else {
obj[i] = filteredItem; // 更新过滤后的元素
}
}
return obj;
}
// 普通对象处理:遍历所有键,递归过滤值
const keys = Object.keys(obj);
for (const key of keys) {
const value = obj[key];
// 递归过滤子值
const filteredValue = deepFilterEmpty(value);
// 若子值为空值,从当前对象中删除该键
if (filteredValue === null || filteredValue === undefined) {
delete obj[key];
} else {
obj[key] = filteredValue; // 更新过滤后的值
}
}
return obj;
}
// 示例
const data = {
a: 1,
b: null,
c: undefined,
d: {
e: 2,
f: null,
g: [3, undefined, { h: 4, i: null }]
},
j: []
};
console.log('过滤前:', JSON.stringify(data, null, 2));
const filteredData = deepFilterEmpty(data);
console.log('过滤后:', JSON.stringify(filteredData, null, 2));
// 预期输出: {"a":1,"d":{"e":2,"g":[3,{"h":4}]}}Promise 顺序并发执行
实现一个函数,控制并发请求,限制最大并发数并按顺序返回结果。
/**
* 控制并发请求,限制最大并发数并按顺序返回结果
* @param {Array<() => Promise<any>>} requests - 网络请求数组,每个元素是返回 Promise 的函数
* @param {number} limit - 最大并发限制数
* @returns {Promise<Array<any>>} - 返回按请求顺序排列的结果数组,支持链式调用
*/
function requestWithLimit(requests, limit) {
return new Promise((resolve, reject) => {
const results = []; // 存储最终结果(按原始请求顺序)
let currentIndex = 0; // 当前需要启动的请求索引
let completedCount = 0; // 已完成的请求数量
let runningCount = 0; // 正在运行的请求数量
// 启动下一个请求
const runNext = () => {
// 如果所有请求都已完成,则解决 Promise
if (completedCount === requests.length) {
resolve(results);
return;
}
// 如果还有请求未启动,并且当前运行的请求数量未达到限制
while (currentIndex < requests.length && runningCount < limit) {
const requestIndex = currentIndex; // 记录当前请求的原始索引
const requestFn = requests[currentIndex];
currentIndex++;
runningCount++;
// 执行请求
requestFn()
.then(res => {
results[requestIndex] = res; // 按原始索引存储结果
})
.catch(err => {
results[requestIndex] = err; // 错误也按原始索引存储
// 可以选择在这里 reject(err) 立即中断所有请求,或者继续完成其他请求
})
.finally(() => {
runningCount--; // 运行中的请求数量减一
completedCount++; // 完成的请求数量加一
runNext(); // 尝试启动下一个请求
});
}
};
// 启动初始的并发请求
runNext();
});
}
// 模拟网络请求函数
const mockRequest = (id, delay, shouldFail = false) => {
return () => new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
console.log(`Request ${id} failed.`);
reject(new Error(`Error from request ${id}`));
} else {
console.log(`Request ${id} completed.`);
resolve(`Data from request ${id}`);
}
}, delay);
});
};
// 模拟5个网络请求
const requests = [
mockRequest(1, 1000),
mockRequest(2, 500, true), // 模拟失败
mockRequest(3, 1200),
mockRequest(4, 800),
mockRequest(5, 700),
];
// 限制最大并发数为2
requestWithLimit(requests, 2)
.then(results => {
console.log('按顺序返回的结果:', results);
})
.catch(error => {
console.error('发生错误:', error);
});解析 URL Params 为对象
将 URL 中的查询参数解析为 JavaScript 对象。
/**
* 解析 URL 中的查询参数为对象
* @param {string} url 完整的 URL 字符串
* @returns {object} 包含查询参数的对象
*/
function parseParam(url) {
const paramsObj = {};
// 获取 URL 中 '?' 后面的查询字符串部分
const queryString = url.split('?')[1];
if (!queryString) {
return paramsObj;
}
// 将查询字符串按 '&' 分割成参数对数组
const paramsArr = queryString.split('&');
paramsArr.forEach(param => {
// 处理有值的参数 (key=value)
if (param.includes('=')) {
let [key, val] = param.split('=');
val = decodeURIComponent(val); // 解码 URL 编码的字符
// 尝试将值转换为数字,如果转换失败则保留原字符串
val = /^\d+(\.\d+)?$/.test(val) ? parseFloat(val) : val;
// 如果 key 已经存在,则将值转换为数组并添加新值
if (Object.prototype.hasOwnProperty.call(paramsObj, key)) {
paramsObj[key] = [].concat(paramsObj[key], val);
} else {
paramsObj[key] = val;
}
} else {
// 处理没有值的参数 (key),约定为 true
paramsObj[param] = true;
}
});
return paramsObj;
}
// 示例
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
console.log('解析 URL 参数:', parseParam(url));
/* 预期结果
{
user: 'anonymous',
id: [ 123, 456 ],
city: '北京',
enabled: true
}
*/深拷贝 (Deep Clone)
深拷贝创建一个独立于原对象的新对象,新对象的所有属性(包括嵌套的对象和数组)都是原对象的副本,而不是引用。
递归实现深拷贝 (不处理循环引用)
/**
* 递归实现深拷贝(不处理循环引用)
* @param {*} obj 要拷贝的对象
* @returns {*} 拷贝后的新对象
*/
function deepCopy(obj) {
// 如果不是对象或为 null,直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
let newObj = Array.isArray(obj) ? [] : {};
for (const key in obj) {
// 确保只拷贝对象自身的属性
if (Object.prototype.hasOwnProperty.call(obj, key)) {
newObj[key] = deepCopy(obj[key]); // 递归拷贝
}
}
return newObj;
}
// 示例
const objA = { a: 1, b: { c: 2 } };
const objB = deepCopy(objA);
console.log('深拷贝结果:', objB);
console.log('objA.b === objB.b:', objA.b === objB.b); // false优化版深拷贝 (处理循环引用及特殊对象)
使用 WeakMap 来存储已拷贝的对象,以解决循环引用问题。同时处理 RegExp、Date、Map、Set 等特殊对象。
/**
* 优化版深拷贝,处理循环引用及特殊对象
* @param {*} obj 要拷贝的对象
* @param {WeakMap} [map] 用于存储已拷贝对象的 WeakMap,防止循环引用
* @returns {*} 拷贝后的新对象
*/
function deepClone(obj, map = new WeakMap()) {
// 1. 基本类型和 null 直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 2. 处理已拷贝过的对象(循环引用)
if (map.has(obj)) {
return map.get(obj);
}
let newObj;
// 3. 处理特殊对象类型
if (obj instanceof RegExp) {
newObj = new RegExp(obj);
} else if (obj instanceof Date) {
newObj = new Date(obj);
} else if (obj instanceof Map) {
newObj = new Map();
map.set(obj, newObj); // 先存储,再递归拷贝其内容
for (const [key, value] of obj) {
newObj.set(key, deepClone(value, map));
}
return newObj;
} else if (obj instanceof Set) {
newObj = new Set();
map.set(obj, newObj); // 先存储,再递归拷贝其内容
for (const value of obj) {
newObj.add(deepClone(value, map));
}
return newObj;
} else if (Array.isArray(obj)) {
newObj = [];
} else {
newObj = {};
}
// 4. 存储新对象到 WeakMap,用于处理循环引用
map.set(obj, newObj);
// 5. 递归拷贝普通对象或数组的属性/元素
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
newObj[key] = deepClone(obj[key], map);
}
}
return newObj;
}
// 示例
const testObj = {
num: 123,
date: new Date(),
reg: /abc/g,
m: new Map([['a', 1]]),
s: new Set([1, 2])
};
testObj.target = testObj; // 创建循环引用
const clonedObj = deepClone(testObj);
console.log('优化版深拷贝结果:', clonedObj);
console.log('clonedObj.target === clonedObj:', clonedObj.target === clonedObj); // true (因为处理了循环引用)
console.log('clonedObj.date === testObj.date:', clonedObj.date === testObj.date); // falseJSON.parse(JSON.stringify(obj))
这是一种简单的深拷贝方法,但有局限性:
- 无法拷贝函数、
undefined、Symbol类型的值(它们会在序列化过程中丢失)。 - 无法处理循环引用(会报错)。
- 无法拷贝
Date对象(会变成字符串)。 - 无法拷贝
RegExp对象(会变成空对象)。
lodash 库
使用第三方库 lodash 提供的 _.cloneDeep 方法是进行深拷贝的常用且健壮的方式。
// 假设已引入 lodash 库
// var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log('lodash 深拷贝:', obj1.b.f === obj2.b.f); // false数组扁平化 (Flatten Array)
将多维数组转换为一维数组。
递归实现
/**
* 递归实现数组扁平化
* @param {Array} arr 要扁平化的数组
* @returns {Array} 扁平化后的一维数组
*/
function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i])); // 递归处理子数组
} else {
result.push(arr[i]);
}
}
return result;
}
// 示例
console.log('递归扁平化:', flatten([1, [2, 3], [4, [5]]])); // [1, 2, 3, 4, 5]Array.prototype.some + Array.prototype.concat + 扩展运算符
/**
* 使用 some、concat 和扩展运算符实现数组扁平化
* @param {Array} arr 要扁平化的数组
* @returns {Array} 扁平化后的一维数组
*/
function flattenWithSome(arr) {
// 只要数组中还存在数组元素,就继续扁平化
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr); // 使用 concat 和扩展运算符将所有子数组展开一层
}
return arr;
}
// 示例
console.log('some + concat 扁平化:', flattenWithSome([1, [2, 3], [4, [5]]])); // [1, 2, 3, 4, 5]toString() + split() + map() (仅适用于简单数据)
这种方法会将所有元素转换为字符串,然后分割,再尝试转换回数字。不适用于包含对象或复杂类型的数组。
/**
* 使用 toString() 扁平化数组(仅适用于简单数据类型)
* @param {Array} arr 要扁平化的数组
* @returns {Array} 扁平化后的一维数组
*/
function flattenToString(arr) {
return arr.toString().split(',').map(item => Number(item));
}
// 示例
console.log('toString 扁平化:', flattenToString([1, [2, 3], [4, [5]]])); // [1, 2, 3, 4, 5]ES6 Array.prototype.flat()
ES6 提供了 flat() 方法,可以指定扁平化的深度。Infinity 表示彻底扁平化。
/**
* 使用 ES6 Array.prototype.flat() 扁平化数组
* @param {Array} arr 要扁平化的数组
* @returns {Array} 扁平化后的一维数组
*/
function flattenWithFlat(arr) {
return arr.flat(Infinity);
}
// 示例
console.log('flat(Infinity) 扁平化:', flattenWithFlat([1, [2, 3], [4, [5]]])); // [1, 2, 3, 4, 5]将数组转化为树结构
将一个扁平的数组结构(通常包含 id 和 pid 字段)转换为树形结构。
/**
* 将扁平数组转换为树形结构
* @param {Array<object>} arr 包含 id 和 pid 的扁平数组
* @returns {Array<object>} 树形结构数组
*/
function toTreeStruct(arr) {
const result = [];
if (!Array.isArray(arr)) {
return result;
}
const map = {}; // 用于快速查找节点
arr.forEach(item => {
map[item.id] = item; // 以 id 为键存储节点
});
arr.forEach(item => {
const parent = map[item.pid]; // 查找当前节点的父节点
if (parent) {
// 如果存在父节点,则将当前节点添加到父节点的 children 数组中
(parent.children || (parent.children = [])).push(item);
} else {
// 如果没有父节点(即 pid 不存在或为根节点标识),则将其视为根节点
result.push(item);
}
});
return result;
}
// 示例
const flatArray = [
{ id: 1, pid: 0, name: 'Node 1' },
{ id: 2, pid: 1, name: 'Node 1-1' },
{ id: 3, pid: 1, name: 'Node 1-2' },
{ id: 4, pid: 2, name: 'Node 1-1-1' },
{ id: 5, pid: 0, name: 'Node 2' },
];
console.log('数组转树结构:', JSON.stringify(toTreeStruct(flatArray), null, 2));数组复杂数据去重
对包含复杂数据类型(如对象)的数组进行去重。
/**
* 对数组进行去重,支持复杂数据类型
* @returns {Array} 去重后的新数组
*/
Array.prototype.myUnique = function() {
const seen = new Set(); // 使用 Set 存储已见过的元素的字符串表示
return this.filter(item => {
// 将复杂数据类型转换为 JSON 字符串进行比较
const newItem = typeof item === 'object' && item !== null ? JSON.stringify(item) : item;
if (seen.has(newItem)) {
return false; // 已存在,过滤掉
} else {
seen.add(newItem); // 不存在,加入 Set
return true; // 保留
}
});
};
// 示例
const complexArr = [
1, 2, 1, { a: 1 }, { a: 1 },
{ b: 2, c: { d: 3 } }, { b: 2, c: { d: 3 } },
[1, 2], [1, 2]
];
console.log('复杂数组去重:', complexArr.myUnique());
// 预期输出: [1, 2, {a:1}, {b:2, c:{d:3}}, [1,2]] (注意对象和数组的 JSON.stringify 顺序可能影响结果)数据类型的判断
JavaScript 中判断数据类型有多种方式,各有优缺点。
typeof 运算符
typeof 运算符返回一个表示操作数类型的字符串。
- 优点:简单快捷,适用于判断基本数据类型。
- 缺点:
- 对
null返回"object"。 - 对数组、对象、
Date、RegExp等引用类型都返回"object",无法区分具体类型。 - 对函数返回
"function"。
- 对
console.log('typeof 1:', typeof 1); // "number"
console.log('typeof "hello":', typeof "hello"); // "string"
console.log('typeof true:', typeof true); // "boolean"
console.log('typeof undefined:', typeof undefined); // "undefined"
console.log('typeof Symbol():', typeof Symbol()); // "symbol"
console.log('typeof null:', typeof null); // "object" (历史遗留问题)
console.log('typeof {}:', typeof {}); // "object"
console.log('typeof []:', typeof []); // "object"
console.log('typeof function(){}:', typeof function(){}); // "function"instanceof 运算符
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
- 优点:可以正确判断引用数据类型的具体类型(如
Array、Date、自定义类实例)。 - 缺点:
- 不能判断基本数据类型。
- 在多全局环境(如
iframe)下可能失效,因为每个全局环境有自己的Array.prototype。
const arr = [];
const date = new Date();
class MyClass {}
const myInstance = new MyClass();
console.log('arr instanceof Array:', arr instanceof Array); // true
console.log('date instanceof Date:', date instanceof Date); // true
console.log('myInstance instanceof MyClass:', myInstance instanceof MyClass); // true
console.log('1 instanceof Number:', 1 instanceof Number); // false (基本类型)Object.prototype.toString.call()
Object.prototype.toString 方法返回一个表示该对象的字符串。通过 call 方法改变 this 指向,可以获取准确的类型字符串。
- 优点:可以准确判断所有内置数据类型(包括基本类型和引用类型)。
- 缺点:相对
typeof略显繁琐。
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
}
console.log('getType(1):', getType(1)); // "Number"
console.log('getType("hello"):', getType("hello")); // "String"
console.log('getType(true):', getType(true)); // "Boolean"
console.log('getType(undefined):', getType(undefined)); // "Undefined"
console.log('getType(null):', getType(null)); // "Null"
console.log('getType({}):', getType({})); // "Object"
console.log('getType([]):', getType([])); // "Array"
console.log('getType(new Date()):', getType(new Date())); // "Date"
console.log('getType(/abc/):', getType(/abc/)); // "RegExp"
console.log('getType(function(){}):', getType(function(){})); // "Function"toString 是 Object 的原型方法,而 Array、Function 等类型作为 Object 的实例,都重写了 toString 方法。因此,直接调用 [].toString() 会得到 "",function(){}.toString() 会得到函数体字符串。通过 Object.prototype.toString.call() 可以强制调用 Object 上的原始 toString 方法,从而获取内部 [[Class]] 属性。