封装通用的JS日期格式化工具函数:支持自定义掩码

作者:橙子

时间:2026-11-12

浏览:650

本文详细讲解如何封装一个高效、轻量且零依赖的JavaScript通用日期格式化工具函数,支持任意自定义掩码(YYYY-MM-DD、HH:mm:ss等),具备防二次污染的单次正则分词机制及完善的输入防御与兼容性。

文章目录
JavaScript / TypeScript 阅读时长:约 8 分钟 适用场景:前端工程、Node.js服务端、零依赖业务模块
在前端开发中,日期展示往往需要根据业务场景自由切换格式,例如表格中的标准日期、操作日志的精确时间戳、或者社交动态的精简月日。通过原生 JavaScript 封装一个通用的 formatDate 工具函数,不仅能以不到 1KB 的精炼体积彻底摆脱对庞大三方库的依赖,还能支持任意自定义掩码与多态输入自动兼容。本文将深入讲解其核心设计逻辑与完整工程实现。
封装通用的JS日期格式化工具函数:支持自定义掩码
图 0:formatDate 自定义掩码格式化工具全貌设计与输入输出链路

设计目标与掩码规则定义

在许多中小型项目或独立组件库中,为了简单的日期格式化而引入体积数十乃至数百 KB 的第三方库(如 Moment.js)会导致包体积严重膨胀。即便使用 Day.js,在不需要复杂时区推算或日期加减计算的只读展示场景下,依旧存在外部依赖引入与版本维护成本。

一个设计优秀的日期格式化函数,应当满足三个核心目标:零外部依赖、支持直观的自定义掩码模式(如 YYYY-MM-DD HH:mm:ss)、以及稳健的异常防御能力。

掩码(Mask)是格式化字符串的占位符模式。为了保持与行业通用的时间标记约定一致,我们定义一套规范的标记符号体系:

掩码标记 含义 取值范围 / 表现形式 示例结果
YYYY 四位完整年份 如 2026 2026
YY 两位简写年份 后两位数字 26
MM / M 月份 双位补零 01-12 / 单数 1-12 05 / 5
DD / D 天数(当月第几日) 双位补零 01-31 / 单数 1-31 08 / 8
HH / H 24小时制时 双位补零 00-23 / 单数 0-23 09 / 9
mm / m 分钟 双位补零 00-59 / 单数 0-59 07 / 7
ss / s 秒数 双位补零 00-59 / 单数 0-59 05 / 5
SSS 毫秒 三位补零 000-999 042
W 星期 中文星期几 星期五

格式化函数的执行链路与架构

格式化工具函数的核心执行逻辑可解耦为清晰的流水线架构。整个过程包含四个阶段:入参归一化、时间分量提取、标记字典组装,以及最后的单次正则词法替换。

封装通用的JS日期格式化工具函数:支持自定义掩码
图 1:formatDate 函数完整执行流与数据状态迁移

每个阶段各司其职,保证了职责的清晰:

  • 输入归一化(Normalize):函数不仅需要支持原生 Date 对象,还必须兼容数字型时间戳(13 位毫秒、10 位秒)以及符合 ISO 格式的日期字符串。对于非法的入参(如 null、undefined 或无法解析的乱码),在第一阶段迅速拦截并返回预设的兜底结果。
  • 分量提取(Extract):通过 Date.prototype.getFullYear()、getMonth()、getDate() 等原生方法获取各时间维度的数字基底,并根据掩码要求进行截取与按位补零。
  • 映射字典组装(Map):将提取好的分量按照预设标记构建成 Key-Value 字典对象。
  • 单次替换(Replace):利用预编译的全局正则一次性匹配掩码中的占位符,完成最终渲染。

为什么不能使用多次链式 replace():二次污染原理

初学者在封装日期格式化工具时,最常犯的错误便是通过链式调用 replace 来依次替换标记:

// 存在严重 Bug 的典型错误实现
function badFormat(date, mask) {
  return mask
    .replace(/YYYY/g, date.getFullYear())
    .replace(/YY/g, String(date.getFullYear()).slice(-2))
    .replace(/MM/g, padZero(date.getMonth() + 1))
    .replace(/m/g, date.getMinutes()); // 灾难来源!
}
封装通用的JS日期格式化工具函数:支持自定义掩码
图 2:掩码替换机制对比:单次正则分词 vs 链式多次替换

这种串行替换方式存在致命的二次污染(Cascade Contamination)问题:

  1. 已替换内容被破坏:若先替换 YYYY 为 2026,如果后续存在其它数字替换逻辑,已生成的 2026 就可能被意外匹配改写。
  2. 大小写与短标记冲突:例如月份与分钟的单字母标记。当把掩码中的 mm 替换为具体数值(如 05)后,若后续执行 replace(/m/g, ...),正则引擎无法区分哪个是原始占位符、哪个是新生成的数字,会将 05 中的 5 甚至前置文本破坏。
  3. 替换顺序极其脆弱:开发者不得不小心翼翼地安排替换顺序(如必须先替换四位再替换两位),但只要掩码稍微复杂或加入新标记,顺序依赖就会彻底崩溃。
核心解决之道:单次正则全集匹配

正确做法是构造一个包含所有支持标记的全局正则表达式,例如 /(YYYY|YY|MM|M|DD|D|HH|H|mm|m|ss|s|SSS|W)/g。并且,较长字符的模式必须排在较短字符前面(例如 YYYY 排在 YY 前,MM 排在 M 前)。正则引擎在执行单次遍历时,每个字符位置只被消费一次,替换后的文本绝不会再次参与下一轮匹配,从底层根除任何二次污染的可能。

完整通用函数实现与代码详解

结合上述设计原则,下面是生产环境可直接投入使用的完整 JavaScript 函数实现。支持 ES6+ 语法,无任何第三方包依赖:

/**
 * 补零辅助函数:确保数值字符串达到指定最小位数
 * @param {number | string} val - 待补齐的数值
 * @param {number} [len=2] - 目标位宽
 * @returns {string}
 */
function padZero(val, len = 2) {
  return String(val).padStart(len, '0');
}

/**
 * 跨平台日期输入归一化解析
 * @param {any} input - 输入的日期对象、时间戳或日期字符串
 * @returns {Date | null} 有效的 Date 实例,若无效则返回 null
 */
function normalizeDate(input) {
  if (!input && input !== 0) return null;
  if (input instanceof Date) {
    return isNaN(input.getTime()) ? null : input;
  }

  // 兼容 10 位秒级时间戳:若数值长度为 10 位,自动升维到毫秒
  if (typeof input === 'number') {
    const ms = input.toString().length === 10 ? input * 1000 : input;
    const d = new Date(ms);
    return isNaN(d.getTime()) ? null : d;
  }

  if (typeof input === 'string') {
    // 纯数字字符串时间戳处理
    if (/^\d+$/.test(input.trim())) {
      const num = Number(input.trim());
      const ms = input.trim().length === 10 ? num * 1000 : num;
      const d = new Date(ms);
      return isNaN(d.getTime()) ? null : d;
    }
    // 兼容 iOS Safari 无法解析 "YYYY-MM-DD" 的情况,替换为 "YYYY/MM/DD"
    const sanitized = input.replace(/-/g, '/');
    const d = new Date(sanitized);
    if (!isNaN(d.getTime())) return d;

    // 回退尝试原生解析(适用于 ISO 8601 等标准格式)
    const fallback = new Date(input);
    return isNaN(fallback.getTime()) ? null : fallback;
  }

  return null;
}

/**
 * 掩码标记提取正则表达式:长标记优先排序
 */
const TOKEN_REGEX = /(YYYY|YY|MM|M|DD|D|HH|H|mm|m|ss|s|SSS|W)/g;

/**
 * 中文星期映射表
 */
const WEEK_MAP = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];

/**
 * 通用日期格式化函数
 * @param {Date | number | string} dateInput - 输入的日期
 * @param {string} [mask='YYYY-MM-DD HH:mm:ss'] - 自定义格式化掩码
 * @param {string} [fallback=''] - 非法日期时的兜底返回值
 * @returns {string} 格式化后的时间字符串
 */
function formatDate(dateInput, mask = 'YYYY-MM-DD HH:mm:ss', fallback = '') {
  const date = normalizeDate(dateInput);
  if (!date) return fallback;

  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();
  const hours = date.getHours();
  const minutes = date.getMinutes();
  const seconds = date.getSeconds();
  const milliseconds = date.getMilliseconds();
  const dayOfWeek = date.getDay();

  // 构建各占位符与格式化值的映射字典
  const tokenMap = {
    YYYY: String(year),
    YY: String(year).slice(-2),
    MM: padZero(month, 2),
    M: String(month),
    DD: padZero(day, 2),
    D: String(day),
    HH: padZero(hours, 2),
    H: String(hours),
    mm: padZero(minutes, 2),
    m: String(minutes),
    ss: padZero(seconds, 2),
    s: String(seconds),
    SSS: padZero(milliseconds, 3),
    W: WEEK_MAP[dayOfWeek]
  };

  // 单次全局正则替换,回调原子取值
  return mask.replace(TOKEN_REGEX, (token) => tokenMap[token] ?? token);
}

// 导出通用工具
if (typeof module !== 'undefined' && module.exports) {
  module.exports = { formatDate, normalizeDate, padZero };
}

边界情况与防御性处理机制

在真实业务工程中,输入的数据往往来自不可控的后端接口、用户输入或未对齐的第三方协议。因此,健壮的日期函数必须有深度的容错防御设计。

封装通用的JS日期格式化工具函数:支持自定义掩码
图 3:输入多态防御与异常容错决策树

1. 时间戳长度智能识别

在很多后端服务(如 PHP、Go 等)中,时间戳默认以“秒”(10位数值,如 1773289888)返回,而 JavaScript 的 new Date(ms) 接收的是毫秒数(13位)。

如果不做兼容,直接传入 10 位时间戳会导致 new Date(1773289888) 被解释为 1970 年初的某个时间。在 normalizeDate 内部,我们通过检查字符串化后的长度是否为 10 位,自动将其乘以 1000 升维为毫秒,大幅提升了对不同后端约定的适应能力。

2. iOS Safari 的日期字符串兼容性

早期的 WebKit 引擎及部分移动端 iOS Safari 浏览器在执行 new Date("2026-11-11 12:00:00") 时,会将其判定为 Invalid Date,而无法正确解析短横线 - 分隔的年月日时间格式。将连字符统一替换为斜杠 /(即 "2026/11/11 12:00:00")能够保持跨平台的解析一致性。

3. 杜绝未捕获异常与脏字符串

如果传入 null、undefined、空字符串或诸如 "abc" 等非法格式,原生 Date 会生成一个 NaN 时间。若继续调用 getFullYear(),输出就会变成极度难看的 NaN-aN-aN,甚至直接导致下游调用方抛出未捕获错误。

通过 isNaN(date.getTime()) 进行第一道防线阻断,一旦不合法即刻返回预设的 fallback(默认为空字符串 ""),确保用户界面不会因脏数据而崩溃。

实战应用与常用掩码速查

封装好的 formatDate 函数不仅语义直观,而且在各种复杂的前端展示需求下都能游刃有余。下表总结了最常用的业务掩码模式及其调用示例:

封装通用的JS日期格式化工具函数:支持自定义掩码
图 4:掩码规则速查表与高频业务场景组合全景

常用场景调用范例

// 1. 标准表格日期时间
formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss');
// => "2026-11-11 14:35:08"

// 2. 毫秒级性能日志或接口调用记录
formatDate(Date.now(), 'YYYY-MM-DD HH:mm:ss.SSS');
// => "2026-11-11 14:35:08.042"

// 3. 中文社交卡片展示
formatDate('2026-05-08 09:07:00', 'YYYY年M月D日 W HH:mm');
// => "2026年5月8日 星期五 09:07"

// 4. 紧凑型移动端列表项
formatDate(1773289888, 'MM/DD HH:mm'); // 自动识别10位秒时间戳
// => "03/12 12:31"

// 5. 极端非法输入防御
formatDate('invalid_date_string', 'YYYY-MM-DD', '--');
// => "--" (优雅降级,不抛错)
开发实践总结

在前端工程化演进中,工具函数的封装不仅是为了“写出代码”,更是为了兼顾代码体积、运行性能与防错边界。通过使用“单次全集正则分词替换”取代容易产生级联破坏的多次 replace,结合统一的“输入归一化防御层”,可以用仅仅数十行核心代码打造出既健壮又易扩展的高可用时间工具。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章