普通视图

发现新文章,点击刷新页面。
昨天以前首页

妙用 localeCompare 获取汉字拼音首字母

作者 jump_jump
2025年10月9日 21:21

在前端开发中,开发者通常会使用 localeCompare 来进行中文字符的排序比较。但 localeCompare 还有一种较为少见的应用场景 —— 通过获取中文字符的拼音首字母来实现检索功能。本文将详细介绍这一实用技巧及其应用。

原理

localeCompare 方法允许字符串按特定语言环境的排序规则进行比较。在中文环境下,它会默认按照汉字的拼音顺序进行排序。基于这一特性:

  1. 准备一组具有代表性的汉字作为基准点(每个字代表一个拼音首字母的起始位置)
  2. 将目标汉字与这些基准字进行 localeCompare 比较
  3. 根据比较结果确定目标汉字的拼音首字母范围

这种方法无需依赖第三方拼音库,实现简单且轻量,适合大多数基础场景。

应用场景

获取汉字拼音首字母在中文应用开发中有广泛的应用场景:

  • 联系人列表排序:按拼音首字母对联系人进行分组排序
  • 城市选择器:按首字母索引快速定位城市
  • 拼音搜索:将拼音首字母存储到数据库中,用户输入拼音首字母即可检索相关内容
  • 数据分类展示:将中文数据按拼音首字母进行分类展示

核心代码

下面是获取汉字拼音首字母的核心函数实现:

/**
 * 获取汉字的拼音首字母
 * @param chineseChar 中文字符串,若传入多字符则只取第一个字符
 * @param useUpperCase 是否返回大写字母,默认为false(小写)
 * @returns 拼音首字母,若无法识别则返回空字符串
 */
export const getTheFirstLetterForPinyin = (chineseChar: string = '', useUpperCase: boolean = false): string => {
    // 兼容性检查:确保浏览器支持 localeCompare 方法
    if (!String.prototype.localeCompare) {
        return '';
    }

    // 参数验证:确保输入为有效字符串
    if (typeof chineseChar !== 'string' || !chineseChar.length) {
        return '';
    }

    // 准备用于比较的字母表和基准汉字
    // 注:这些基准汉字分别对应A、B、C...等拼音首字母的起始位置
    const letters = 'ABCDEFGHJKLMNOPQRSTWXYZ'.split('');
    const zh = '阿八嚓哒妸发旮哈讥咔垃痳拏噢妑七呥扨它穵夕丫帀'.split('');

    let firstLetter = '';
    const firstChar = chineseChar[0];

    // 处理字母和数字:直接返回原字符
    if (/^\w/.test(firstChar)) {
        firstLetter = firstChar;
    } else {
        // 处理汉字:通过比较确定拼音首字母范围
        letters.some((item, index) => {
            // 检查当前字符是否在当前基准汉字与下一个基准汉字之间
            if (firstChar.localeCompare(zh[index]) >= 0 && 
                (index === letters.length - 1 || firstChar.localeCompare(zh[index + 1]) < 0)) {
                firstLetter = item;
                return true;
            }
            return false;
        });
    }

    // 根据参数决定返回大写还是小写字母
    return useUpperCase ? firstLetter.toUpperCase() : firstLetter.toLowerCase();
}

实用示例

获取单个汉字的拼音首字母

// 获取单个汉字的拼音首字母
getTheFirstLetterForPinyin('你'); // => 'n'
getTheFirstLetterForPinyin('好', true); // => 'H'
getTheFirstLetterForPinyin('123'); // => '1'
getTheFirstLetterForPinyin(''); // => ''

获取字符串的拼音首字母缩写

/**
 * 获取整个字符串的拼音首字母缩写
 * @param str 输入字符串
 * @returns 拼音首字母缩写
 */
function getPinyinInitials(str: string): string {
    return str.split('')
        .map(char => getTheFirstLetterForPinyin(char))
        .join('');
}

getPinyinInitials('你好世界'); // => 'nhsj'
getPinyinInitials('JavaScript中文教程'); // => 'javascriptzwjc'
getPinyinInitials('加油123'); // => 'jy123'

联系人列表按首字母分组

// 将联系人列表按拼音首字母分组
function groupContactsByInitial(contacts: {name: string}[]): Record<string, {name: string}[]> {
    const groups: Record<string, {name: string}[]> = {};
    
    // 按首字母分组
    contacts.forEach(contact => {
        const initial = getTheFirstLetterForPinyin(contact.name, true);
        if (!groups[initial]) {
            groups[initial] = [];
        }
        groups[initial].push(contact);
    });
    
    return groups;
}

const contacts = [
    {name: '张三'},
    {name: '李四'},
    {name: '王五'},
    {name: '赵六'}
];

// 按首字母分组
const groupedContacts = groupContactsByInitial(contacts);

// 还可以进一步通过 localeCompare 实现组内排序,此处暂时不做操作
/*
结果:
{
    'L': [{name: '李四'}],
    'W': [{name: '王五'}],
    'Z': [{name: '张三'}, {name: '赵六'}]
}
*/

局限性与替代方案

localeCompare 方法在现代浏览器中得到广泛支持(除了 IE11 前的版本不可用之外,其余浏览器均完全支持)。

局限性

  1. 多音字问题:该方法无法处理多音字情况,例如"音乐"会被识别为"yl"而非"yy"
  2. 准确性限制:对于一些生僻字,可能无法准确识别其拼音首字母
  3. 依赖浏览器实现:不同浏览器的 localeCompare 实现可能略有差异

替代方案

如果需要更强大的拼音处理功能,可以考虑使用以下第三方库:

  • pinyin-pro:支持多音字识别、音调标注等高级功能
  • pinyinjs:轻量级的拼音转换库

通过 localeCompare API,可以在不依赖第三方库的情况下,快速实现中文拼音首字母的获取功能,为您的应用添加更加友好的中文处理能力。

超长定时器 long-timeout

作者 jump_jump
2025年10月8日 19:19

在 JavaScript 开发中,定时器是常用的异步编程工具。然而,原生的 setTimeoutsetInterval 存在一个鲜为人知的限制:它们无法处理超过 24.8 天的定时任务。

对于前端开发来说,该限制不太会出现问题,但是需要设置超长定时的后端应用场景,如长期提醒、周期性数据备份、订阅服务到期提醒等,这个限制可能会导致严重的功能缺陷。

JavaScript 定时器的限制

原理

JavaScript 中 setTimeoutsetInterval 的延时参数存在一个最大值限制,这源于底层实现的整数类型限制。具体来说:

// JavaScript 定时器的最大延时值(单位:毫秒)
const TIMEOUT_MAX = 2 ** 31 - 1; // 2147483647 毫秒

// 转换为天数
const MAX_DAYS = TIMEOUT_MAX / 1000 / 60 / 60 / 24; // 约 24.855 天

console.log(TIMEOUT_MAX); // 输出: 2147483647
console.log(MAX_DAYS);   // 输出: 24.855134814814818

这一限制的根本原因在于 JavaScript 引擎内部使用 32 位有符号整数来存储延时值。当提供的延时值超过这个范围时,JavaScript 会将其视为 0 处理,导致定时器立即执行。

问题示例

以下代码演示了超出限制时的问题:

// 尝试设置 30 天的延时(超出 24.8 天的限制)
setTimeout(() => {
  console.log("应该在 30 天后执行");
}, 1000 * 60 * 60 * 24 * 30); // 2592000000 毫秒

// 实际结果:回调函数会立即执行,而不是在 30 天后

在控制台中执行上述代码,会发现回调函数立即执行,而不是像预期那样在 30 天后执行。这是因为 2592000000 毫秒超过了 2147483647 毫秒的最大值限制。

long-timeout 库

long-timeout 是一个专门解决 JavaScript 定时器时间限制问题的轻量级库。它提供了与原生 API 兼容的接口,同时支持处理超过 24.8 天的延时任务。

主要特性

  • 完全兼容原生 setTimeoutsetInterval API
  • 支持任意时长的延时,不受 24.8 天限制
  • 轻量级实现,无外部依赖
  • 同时支持 Node.js 和浏览器环境
  • 提供与原生方法对应的清除定时器函数

安装与基本使用

安装

可以通过 npm 或 yarn 安装 long-timeout 库:

# 使用 npm
npm install long-timeout

# 使用 yarn
yarn add long-timeout

pnpm add long-timeout

基本用法

long-timeout 库提供了与原生 API 几乎相同的接口,使用非常简单:

// 引入 long-timeout 库
import lt from 'long-timeout';

// 设置一个 30 天的超时定时器
// 返回一个定时器引用,用于清除定时器
const timeoutRef = lt.setTimeout(() => {
  console.log('30 天后执行的代码');
}, 1000 * 60 * 60 * 24 * 30); // 2592000000 毫秒

// 清除超时定时器
// lt.clearTimeout(timeoutRef);

// 设置一个每 30 天执行一次的间隔定时器
const intervalRef = lt.setInterval(() => {
  console.log('每 30 天执行一次的代码');
}, 1000 * 60 * 60 * 24 * 30);

// 清除间隔定时器
// 同上
// lt.clearInterval(intervalRef);

实现原理

long-timeout 库的核心实现原理是将超长延时分解为多个不超过 24.8 天的小延时,通过递归调用 setTimeout 来实现对超长延时的支持。同时 node-cron 库也是基于该原理实现的。

核心实现代码

以下是 long-timeout 库的核心实现逻辑:

// 定义 32 位有符号整数的最大值
const TIMEOUT_MAX = 2147483647;

// 定时器构造函数
function Timeout(after, listener) {
  this.after = after;
  this.listener = listener;
  this.timeout = null;
}

// 启动定时器的方法
Timeout.prototype.start = function() {
  // 如果延时小于最大值,直接使用 setTimeout
  if (this.after <= TIMEOUT_MAX) {
    this.timeout = setTimeout(this.listener, this.after);
  } else {
    const self = this;
    // 否则,先设置一个最大值的延时,然后递归调用
    this.timeout = setTimeout(function() {
      // 减去已经等待的时间
      self.after -= TIMEOUT_MAX;
      // 继续启动定时器
      self.start();
    }, TIMEOUT_MAX);
  }
};

// 清除定时器的方法
Timeout.prototype.clear = function() {
  if (this.timeout !== null) {
    clearTimeout(this.timeout);
    this.timeout = null;
  }
};

工作流程图解

long-timeout 库的工作流程可以概括为以下几个步骤:

  1. 接收用户设置的延时时间和回调函数
  2. 检查延时是否超过 2147483647 毫秒(约 24.8 天)
  3. 如果未超过最大值,直接使用原生 setTimeout
  4. 如果超过最大值,将延时分解为多个最大值的段,通过递归调用实现
  5. 每完成一个时间段,更新剩余延时并继续设置下一个定时器
  6. 当所有时间段完成后,执行用户提供的回调函数
[用户设置超长延时][检查是否超过 TIMEOUT_MAX] ── 否 ─→ [直接使用 setTimeout]
                       └── 是 ─→ [分解为多个 TIMEOUT_MAX 段][递归调用 setTimeout][所有段完成后执行回调]

注意事项与最佳实践

内存管理

对于长时间运行的应用,应当注意及时清除不再需要的定时器,以避免内存泄漏:

import lt from 'long-timeout';

let timeoutRef = lt.setTimeout(() => {
  console.log('任务执行');
}, 1000 * 60 * 60 * 24 * 30); // 30 天

// 当不再需要该定时器时,及时清除
function cancelTask() {
  if (timeoutRef) {
    lt.clearTimeout(timeoutRef);
    timeoutRef = null; // 释放引用
    console.log('定时器已清除');
  }
}

应用重启的处理

需要注意的是,long-timeout 仅在应用运行期间有效。如果应用重启或进程终止,所有未执行的定时器都会丢失。对于需要持久化的定时任务,建议结合数据库存储:

// 引入 long-timeout 库
import lt from 'long-timeout';
// 假设的数据库模块
import db from './database'; 

// 从数据库加载未完成的定时任务
async function loadPendingTasks() {
  const tasks = await db.getPendingTasks();
  
  tasks.forEach(task => {
    const now = Date.now();
    const delay = task.executeTime - now;
    
    if (delay > 0) {
      // 重新设置定时器
      const timeoutId = lt.setTimeout(async () => {
        await executeTask(task.id);
        await db.markTaskAsCompleted(task.id);
      }, delay);
      
      // 保存 timeoutId 以便后续可能的取消操作
      db.updateTaskTimeoutId(task.id, timeoutId);
    } else {
      // 任务已过期,基于业务和当前时刻来决定是否执行或取消
      // 如电商大促发送短信提醒用户
      
      // 这里简单假设任务已过期,直接执行
      await executeTask(task.id);
      await db.markTaskAsCompleted(task.id);
    }
  });
}

精确性考虑

虽然 long-timeout 成功解决了定时器时间范围的限制问题,但定时器的执行精度仍受 JavaScript 事件循环机制和系统调度的影响。在实际运行中,任务可能无法按照预设时间精准执行。

为了减少系统调度带来的误差,可在每次定时器触发时记录当前时间戳,并在回调函数中计算实际执行时间,以此对时间误差进行补偿。不过这种方法仅能缓解部分精度问题,无法完全消除误差。

对于对计时精度要求高的场景,long-timeout 可能无法满足需求。开发者可以通过以下方案来解决:

  • Web Workers:可在后台线程执行任务,不阻塞主线程,一定程度上能提升计时精度。不过存在通信开销大及实现复杂的问题。
  • Node.js 的 process.hrtime():提供高精度的时间测量,可用于需要精确计时的场景,结合适当的逻辑可实现较精确的定时任务。
  • 操作系统级定时任务:如 Linux 的 cron 或 Windows 的任务计划程序,借助系统层面的调度能力,能保证较高的计时精度,不过需要与应用程序进行交互集成。

替代方案与技术对比

除了 long-timeout 库外,还有其他几种处理超长定时任务的方法:

表格

方案 优点 缺点
long-timeout 库 API 友好,使用简单,轻量级 仅在应用运行期间有效,不支持持久化
自定义递归 setTimeout 不需要额外依赖 实现复杂,管理困难
Web Workers 不阻塞主线程 通信开销大,实现复杂
服务端定时任务 持久化,不受客户端限制 需要服务器资源,网络依赖
浏览器闹钟 API 系统级支持,应用关闭后仍可工作 浏览器兼容性问题,用户权限要求
❌
❌