Skip to content

常见算法 - JavaScript 特性

字数
6568 字
阅读时间
31 分钟

判断循环引用

思路:通过深度优先搜索 (DFS) 遍历对象,并使用一个 parentArr 数组(或 WeakSet)来记录已经访问过的父级对象。如果在遍历过程中发现当前对象与 parentArr 中的某个对象相同,则说明存在循环引用。

javascript
/**
 * 判断对象是否存在循环引用
 * @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)

发布订阅模式是一种松耦合的通信机制,其中发布者和订阅者之间没有直接的依赖关系。发布者发布事件,订阅者监听事件并执行相应的处理函数。

javascript
/**
 * 事件中心类
 */
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)

观察者模式定义了对象之间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。

javascript
/**
 * 观察者类
 */
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 方法。

大数相加

思路:模拟小学数学中的竖式加法,从个位开始逐位相加,并处理进位。

javascript
/**
 * 实现两个大数相加
 * @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"

大数相乘

思路:模拟小学数学中的竖式乘法,将一个数的每一位与另一个数相乘,然后将结果错位相加。

javascript
/**
 * 实现两个大数相乘
 * @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、滚动事件等。

javascript
/**
 * 防抖函数
 * @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)

定义:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。 应用场景:滚动加载、高频点击、游戏射击等。

javascript
/**
 * 节流函数
 * @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
/**
 * 深度比较两个 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 函数,用于深度过滤对象或数组中的空值(nullundefined)。

javascript
/**
 * 深度过滤对象或数组中的空值(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 顺序并发执行

实现一个函数,控制并发请求,限制最大并发数并按顺序返回结果。

javascript
/**
 * 控制并发请求,限制最大并发数并按顺序返回结果
 * @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 对象。

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)

深拷贝创建一个独立于原对象的新对象,新对象的所有属性(包括嵌套的对象和数组)都是原对象的副本,而不是引用。

递归实现深拷贝 (不处理循环引用)

javascript
/**
 * 递归实现深拷贝(不处理循环引用)
 * @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 来存储已拷贝的对象,以解决循环引用问题。同时处理 RegExpDateMapSet 等特殊对象。

javascript
/**
 * 优化版深拷贝,处理循环引用及特殊对象
 * @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); // false

JSON.parse(JSON.stringify(obj))

这是一种简单的深拷贝方法,但有局限性:

  • 无法拷贝函数、undefinedSymbol 类型的值(它们会在序列化过程中丢失)。
  • 无法处理循环引用(会报错)。
  • 无法拷贝 Date 对象(会变成字符串)。
  • 无法拷贝 RegExp 对象(会变成空对象)。

lodash

使用第三方库 lodash 提供的 _.cloneDeep 方法是进行深拷贝的常用且健壮的方式。

javascript
// 假设已引入 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)

将多维数组转换为一维数组。

递归实现

javascript
/**
 * 递归实现数组扁平化
 * @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 + 扩展运算符

javascript
/**
 * 使用 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() (仅适用于简单数据)

这种方法会将所有元素转换为字符串,然后分割,再尝试转换回数字。不适用于包含对象或复杂类型的数组。

javascript
/**
 * 使用 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 表示彻底扁平化。

javascript
/**
 * 使用 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]

将数组转化为树结构

将一个扁平的数组结构(通常包含 idpid 字段)转换为树形结构。

javascript
/**
 * 将扁平数组转换为树形结构
 * @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));

数组复杂数据去重

对包含复杂数据类型(如对象)的数组进行去重。

javascript
/**
 * 对数组进行去重,支持复杂数据类型
 * @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"
    • 对数组、对象、DateRegExp 等引用类型都返回 "object",无法区分具体类型。
    • 对函数返回 "function"
javascript
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 属性是否出现在某个实例对象的原型链上。

  • 优点:可以正确判断引用数据类型的具体类型(如 ArrayDate、自定义类实例)。
  • 缺点
    • 不能判断基本数据类型。
    • 在多全局环境(如 iframe)下可能失效,因为每个全局环境有自己的 Array.prototype
javascript
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 略显繁琐。
javascript
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"

toStringObject 的原型方法,而 ArrayFunction 等类型作为 Object 的实例,都重写了 toString 方法。因此,直接调用 [].toString() 会得到 ""function(){}.toString() 会得到函数体字符串。通过 Object.prototype.toString.call() 可以强制调用 Object 上的原始 toString 方法,从而获取内部 [[Class]] 属性。

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写