优雅的实现一个时间处理插件
1. UMD 模式解析
1.1 立即执行函数 (IIFE)
外层是一个立即执行函数,接收 global 和 factory 两个参数。
global 参数说明:
typeof window !== "undefined" ? window : this;
根据不同的运行环境,global 参数会指向:
-
浏览器环境:
window 对象
-
Node.js 环境:
global 对象
-
其他环境:当前上下文
this
1.2 UMD 模块定义
通过条件判断支持多种模块系统:
if (typeof define === "function" && define.amd) {
// AMD 模式 (RequireJS)
define(function () {
return factory();
});
} else if (typeof module === "object" && module.exports) {
// CommonJS 模式 (Node.js)
module.exports = factory();
} else {
// 浏览器全局变量模式
global.SurveyTimezone = factory();
}
支持的模块系统:
-
AMD:用于 RequireJS 等加载器
-
CommonJS:用于 Node.js 环境
-
全局变量:用于直接在浏览器中使用
1.3 工厂函数解析
工厂函数 factory 返回插件的构造函数:
function () {
'use strict';
// 构造函数定义
function SurveyTimezone(options) {
}
// 原型方法定义
SurveyTimezone.prototype = {
constructor: SurveyTimezone, // 修复 constructor 指向
version: '1.0.0',
_init: function () {
}
}
// 返回构造函数
return SurveyTimezone;
}
核心组成部分:
-
严格模式:使用
'use strict' 确保代码质量
-
构造函数:定义
SurveyTimezone 类
-
原型方法:通过原型链添加共享方法
-
返回值:导出构造函数供外部使用
1.4 执行流程
UMD 模块的加载和执行流程:
-
立即执行:代码加载后立即执行 IIFE
-
环境检测:检测当前支持的模块系统(AMD / CommonJS / 全局变量)
-
工厂调用:执行
factory() 函数,返回 SurveyTimezone 构造函数
-
模块导出:根据环境将构造函数导出到相应位置
(function (global, factory) {
// 环境检测和模块导出逻辑
}(typeof window !== "undefined" ? window : this, function () {
// 工厂函数:创建并返回构造函数
}));
2. 单例模式实现
2.1 为什么使用单例模式?
在时区处理插件中,我们通常只需要一个全局实例来管理配置和状态:
-
避免重复实例:防止多次实例化造成的内存浪费
-
全局状态管理:统一管理时区配置、数据映射表等
-
配置一致性:确保整个应用使用相同的时区设置
2.2 单例模式实现
2.2.1 私有实例存储
// 闭包中的私有变量,存储单例实例
var instance = null;
2.2.2 构造函数实现
function SurveyTimezone(options) {
// 1. 如果已存在实例,直接返回
if (instance) {
return instance;
}
// 2. 确保通过 new 调用
if (!(this instanceof SurveyTimezone)) {
return new SurveyTimezone(options);
}
// 3. 初始化配置
this.options = options || {};
// 4. 保存单例实例
instance = this;
// 5. 执行初始化
this._init();
return instance;
}
实现要点:
-
实例检查:首次检查是否已存在实例,有则直接返回
-
new 检查:确保即使不用
new 关键字也能正常工作
-
配置初始化:保存传入的配置选项
-
实例保存:将当前实例保存到闭包变量中
-
初始化执行:调用内部初始化方法
2.2.3 静态方法
/**
* 获取单例实例
*/
SurveyTimezone.getInstance = function (options) {
if (!instance) {
instance = new SurveyTimezone(options);
}
return instance;
}
/**
* 重置单例(用于测试)
*/
SurveyTimezone.resetInstance = function () {
instance = null;
}
2.3 使用方式
方式一:使用 new 关键字
const instance1 = new SurveyTimezone({ timezone: 'Asia/Shanghai' });
const instance2 = new SurveyTimezone({ timezone: 'America/New_York' });
console.log(instance1 === instance2); // true(返回同一个实例)
方式二:使用 getInstance 静态方法
const instance = SurveyTimezone.getInstance({ timezone: 'Asia/Shanghai' });
方式三:不使用 new(自动转换)
const instance = SurveyTimezone({ timezone: 'Asia/Shanghai' });
2.4 单例模式的优势
| 优势 |
说明 |
| 内存优化 |
只创建一个实例,减少内存占用 |
| 状态一致 |
全局共享同一个实例,避免状态不一致 |
| 易于管理 |
集中管理配置和数据 |
| 防止冲突 |
避免多个实例之间的配置冲突 |
2.5 完整示例
// 第一次创建实例
const timezone1 = new SurveyTimezone({
timezone: 'Asia/Shanghai',
locale: 'zh-CN'
});
// 第二次尝试创建(返回第一次的实例)
const timezone2 = new SurveyTimezone({
timezone: 'America/New_York' // 这个配置会被忽略
});
console.log(timezone1 === timezone2); // true
console.log(timezone1.options.timezone); // 'Asia/Shanghai'
// 重置单例后可以创建新实例
SurveyTimezone.resetInstance();
const timezone3 = new SurveyTimezone({
timezone: 'Europe/London'
});
console.log(timezone1 === timezone3); // false
console.log(timezone3.options.timezone); // 'Europe/London'
2.6 初始化时传入日期时间
从 v1.0.0 开始,SurveyTimezone 支持在初始化时传入日期时间字符串或时间戳,使其更加灵活实用。
2.6.1 初始化方式
构造函数签名:
new SurveyTimezone(input, format)
参数说明:
| 参数 |
类型 |
必填 |
说明 |
input |
string|number|Date|Object |
❌ |
日期时间字符串、时间戳、Date对象或配置对象 |
format |
string |
❌ |
日期格式(当 input 为字符串时使用) |
2.6.2 初始化示例
方式1:传入日期字符串
const tz = new SurveyTimezone('2025-10-28 14:30:45');
console.log(tz.getDate()); // Date 对象
console.log(tz.format()); // '2025-10-28 14:30:45'
方式2:传入时间戳
const tz = new SurveyTimezone(1698484245000);
console.log(tz.getDate()); // Date 对象
console.log(tz.format()); // 对应的日期时间字符串
方式3:传入日期字符串和格式
const tz = new SurveyTimezone('28/10/2025', 'DD/MM/YYYY');
console.log(tz.format('YYYY-MM-DD')); // '2025-10-28'
方式4:传入 Date 对象
const tz = new SurveyTimezone(new Date());
console.log(tz.format()); // 当前时间
方式5:传入配置对象
const tz = new SurveyTimezone({
date: '2025-10-28 14:30:45',
timezone: 'Asia/Shanghai',
locale: 'zh-CN'
});
console.log(tz.getDate()); // Date 对象
console.log(tz.options); // { date: '...', timezone: '...', locale: '...' }
方式6:不传参数(默认当前时间)
const tz = new SurveyTimezone();
console.log(tz.format()); // 当前时间
2.6.3 新增实例方法
getDate() - 获取日期对象
const tz = new SurveyTimezone('2025-10-28 14:30:45');
const date = tz.getDate();
console.log(date); // Date 对象
setDate() - 设置日期(支持链式调用)
const tz = new SurveyTimezone();
// 设置新日期
tz.setDate('2025-12-25 00:00:00');
console.log(tz.format()); // '2025-12-25 00:00:00'
// 链式调用
const result = tz.setDate('2026-01-01').format('YYYY/MM/DD');
console.log(result); // '2026/01/01'
2.6.4 format 方法增强
现在 format 方法可以在不传参数时格式化实例的日期:
const tz = new SurveyTimezone('2025-10-28 14:30:45');
// 格式化实例日期(无参数)
tz.format(); // '2025-10-28 14:30:45'(默认格式)
// 格式化实例日期(指定格式)
tz.format('YYYY年MM月DD日'); // '2025年10月28日'
// 格式化指定日期
tz.format(new Date(), 'YYYY-MM-DD'); // 格式化其他日期
2.6.5 完整工作流示例
// 场景:接收用户输入 → 解析 → 处理 → 格式化输出
// 1. 用户输入欧洲格式日期
const userInput = '28/10/2025';
// 2. 创建实例并解析
const tz = new SurveyTimezone(userInput, 'DD/MM/YYYY');
// 3. 验证解析结果
console.log(tz.getDate()); // Date 对象
// 4. 格式化为不同格式输出
console.log(tz.format('YYYY-MM-DD')); // '2025-10-28'(ISO格式)
console.log(tz.format('YYYY年MM月DD日')); // '2025年10月28日'(中文)
console.log(tz.format('MMM DD, yyyy')); // 'Oct 28, 2025'(英文)
// 5. 修改日期并重新格式化
tz.setDate('2025-12-25');
console.log(tz.format('YYYY年MM月DD日')); // '2025年12月25日'
2.6.6 与单例模式的配合
由于采用单例模式,第一次初始化时传入的日期会被保存,后续创建实例会返回同一个实例:
// 第一次创建,指定日期
const tz1 = new SurveyTimezone('2025-10-28 14:30:45');
console.log(tz1.format()); // '2025-10-28 14:30:45'
// 第二次创建,尝试传入不同日期(但返回的是同一个实例)
const tz2 = new SurveyTimezone('2026-01-01 00:00:00');
console.log(tz2.format()); // '2025-10-28 14:30:45'(仍然是第一次的日期)
console.log(tz1 === tz2); // true(同一个实例)
// 如果需要新的日期,可以使用 setDate 方法
tz2.setDate('2026-01-01 00:00:00');
console.log(tz2.format()); // '2026-01-01 00:00:00'
console.log(tz1.format()); // '2026-01-01 00:00:00'(tz1 也变了,因为是同一个实例)
// 或者先重置单例
SurveyTimezone.resetInstance();
const tz3 = new SurveyTimezone('2026-01-01 00:00:00');
console.log(tz3.format()); // '2026-01-01 00:00:00'(新实例,新日期)
2.6.7 错误处理
当传入无效日期时,会自动使用当前时间:
// 无效日期字符串
const tz1 = new SurveyTimezone('invalid date');
console.log(tz1.getDate()); // 当前时间的 Date 对象
// 重置单例
SurveyTimezone.resetInstance();
// 无效时间戳
const tz2 = new SurveyTimezone(NaN);
console.log(tz2.getDate()); // 当前时间的 Date 对象
2.6.8 方法对比表
| 方法 |
类型 |
参数 |
返回值 |
说明 |
new SurveyTimezone(input, format) |
构造函数 |
日期输入 |
实例 |
创建实例并初始化日期 |
getDate() |
实例方法 |
- |
Date |
获取实例的日期对象 |
setDate(input, format) |
实例方法 |
日期输入 |
this |
设置实例日期,支持链式调用 |
format(date, format) |
实例方法 |
可选 |
string |
格式化日期(无参数时格式化实例日期) |
parse(dateString, format) |
实例方法 |
必填 |
Date|null |
解析日期字符串 |
3. format 方法详解
3.1 方法说明
format 方法用于格式化日期对象,兼容 dayjs 和 laydate 两种流行的日期格式化风格。
方法签名:
// 原型方法
instance.format(date, format)
// 静态方法(向后兼容)
SurveyTimezone.format(date, format)
参数:
| 参数 |
类型 |
必填 |
默认值 |
说明 |
date |
Date |
✅ |
- |
要格式化的 JavaScript Date 对象 |
format |
string |
❌ |
'YYYY-MM-DD HH:mm:ss' |
格式化模板字符串 |
返回值:
-
类型:
string
-
说明:格式化后的日期字符串,无效日期返回空字符串
''
3.2 支持的格式化标记
3.2.1 年份标记
| 标记 |
说明 |
示例输出 |
兼容性 |
YYYY |
四位年份 |
2025 |
dayjs |
yyyy |
四位年份 |
2025 |
laydate |
YY |
两位年份 |
25 |
dayjs |
y |
两位年份 |
25 |
laydate |
3.2.2 月份标记
| 标记 |
说明 |
示例输出 |
兼容性 |
MMM |
英文月份缩写 |
Jan, Feb, Mar... |
通用 |
MM |
两位月份(补零) |
01, 02... 12 |
通用 |
M |
月份(不补零) |
1, 2... 12 |
通用 |
3.2.3 日期标记
| 标记 |
说明 |
示例输出 |
兼容性 |
DD |
两位日期(补零) |
01, 02... 31 |
dayjs |
dd |
两位日期(补零) |
01, 02... 31 |
laydate |
D |
日期(不补零) |
1, 2... 31 |
dayjs |
d |
日期(不补零) |
1, 2... 31 |
laydate |
3.2.4 时间标记
| 标记 |
说明 |
示例输出 |
兼容性 |
HH |
24小时制小时(补零) |
00, 01... 23 |
通用 |
H |
24小时制小时(不补零) |
0, 1... 23 |
通用 |
mm |
分钟(补零) |
00, 01... 59 |
通用 |
m |
分钟(不补零) |
0, 1... 59 |
通用 |
ss |
秒(补零) |
00, 01... 59 |
通用 |
s |
秒(不补零) |
0, 1... 59 |
通用 |
3.2.5 毫秒标记
| 标记 |
说明 |
示例输出 |
兼容性 |
SSS |
三位毫秒 |
000, 001... 999 |
dayjs |
3.3 使用示例
3.3.1 基础用法
const tz = new SurveyTimezone();
const now = new Date('2025-10-28 14:30:45.123');
// 默认格式
tz.format(now); // '2025-10-28 14:30:45'
// 自定义格式
tz.format(now, 'YYYY/MM/DD'); // '2025/10/28'
tz.format(now, 'HH:mm:ss'); // '14:30:45'
3.3.2 dayjs 风格格式
const tz = new SurveyTimezone();
const now = new Date('2025-10-28 14:30:45.123');
tz.format(now, 'YYYY-MM-DD HH:mm:ss'); // '2025-10-28 14:30:45'
tz.format(now, 'YYYY-MM-DD HH:mm:ss.SSS'); // '2025-10-28 14:30:45.123'
tz.format(now, 'YY/M/D H:m:s'); // '25/10/28 14:30:45'
tz.format(now, 'MMM DD, YYYY'); // 'Oct 28, 2025'
3.3.3 laydate 风格格式
const tz = new SurveyTimezone();
const now = new Date('2025-10-28 14:30:45');
tz.format(now, 'yyyy-MM-dd HH:mm:ss'); // '2025-10-28 14:30:45'
tz.format(now, 'yyyy年MM月dd日'); // '2025年10月28日'
tz.format(now, 'y-M-d H:m:s'); // '25-10-28 14:30:45'
tz.format(now, 'dd/MM/yyyy'); // '28/10/2025'
3.3.4 中文日期格式
const tz = new SurveyTimezone();
const now = new Date('2025-10-28 14:30:45');
tz.format(now, 'YYYY年MM月DD日'); // '2025年10月28日'
tz.format(now, 'YYYY年MM月DD日 HH时mm分ss秒'); // '2025年10月28日 14时30分45秒'
tz.format(now, 'yyyy年MM月dd日 HH:mm'); // '2025年10月28日 14:30'
3.3.5 静态方法调用(无需实例化)
const now = new Date('2025-10-28 14:30:45');
// 直接使用静态方法
SurveyTimezone.format(now, 'YYYY-MM-DD'); // '2025-10-28'
SurveyTimezone.format(now, 'yyyy年MM月dd日'); // '2025年10月28日'
SurveyTimezone.format(now, 'MMM DD, yyyy'); // 'Oct 28, 2025'
3.4 常用格式模板
| 格式模板 |
输出示例 |
使用场景 |
YYYY-MM-DD |
2025-10-28 |
标准日期格式 |
YYYY-MM-DD HH:mm:ss |
2025-10-28 14:30:45 |
完整日期时间 |
yyyy年MM月dd日 |
2025年10月28日 |
中文日期 |
MMM DD, yyyy |
Oct 28, 2025 |
英文日期 |
YYYY/MM/DD HH:mm |
2025/10/28 14:30 |
简短日期时间 |
HH:mm:ss |
14:30:45 |
仅时间 |
YY-M-D |
25-10-28 |
简短日期 |
YYYY-MM-DD HH:mm:ss.SSS |
2025-10-28 14:30:45.123 |
带毫秒 |
3.5 错误处理
const tz = new SurveyTimezone();
// 无效日期返回空字符串
tz.format(null); // ''
tz.format(undefined); // ''
tz.format(new Date('invalid')); // ''
tz.format('2025-10-28'); // ''(字符串不是 Date 对象)
// 有效日期
tz.format(new Date()); // '2025-10-28 14:30:45'(当前时间)
3.6 实现原理
format 方法采用正则替换策略实现格式化:
-
标记解析:将格式字符串中的标记(如
YYYY、MM)识别出来
-
长度优先:按标记长度从长到短处理,避免
YYYY 被 YY 误替换
-
顺序处理:依次替换每个标记为对应的日期值
-
类型转换:使用 JavaScript Date 对象的原生方法获取年、月、日等值
关键代码逻辑:
// 标记处理顺序(长的在前,短的在后)
var tokens = ['YYYY', 'yyyy', 'MMM', 'SSS', 'MM', 'DD', 'dd', 'HH', 'mm', 'ss', 'YY', 'M', 'D', 'd', 'H', 'm', 's', 'y'];
// 依次替换每个标记
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (result.indexOf(token) !== -1) {
result = result.replace(new RegExp(token, 'g'), matches[token]());
}
}
3.7 原型方法 vs 静态方法
| 特性 |
原型方法 |
静态方法 |
| 调用方式 |
instance.format() |
SurveyTimezone.format() |
| 是否需要实例化 |
✅ 需要 |
❌ 不需要 |
| 访问实例属性 |
✅ 可以 |
❌ 不可以 |
| 推荐使用场景 |
面向对象编程 |
工具函数调用 |
| 性能 |
略优 |
略低(多一层调用) |
关系说明:
- 静态方法内部调用原型方法实现
- 两种方式返回结果完全一致
- 静态方法保证向后兼容性
// 静态方法实现(调用原型方法)
SurveyTimezone.format = function (date, format) {
return SurveyTimezone.prototype.format.call(null, date, format);
}
4. parse 方法详解
4.1 方法说明
parse 方法用于将日期时间字符串解析为 JavaScript Date 对象,支持多种常见格式的自动识别。
方法签名:
// 原型方法
instance.parse(dateString, format)
// 静态方法(向后兼容)
SurveyTimezone.parse(dateString, format)
参数:
| 参数 |
类型 |
必填 |
默认值 |
说明 |
dateString |
string|number |
✅ |
- |
日期时间字符串或时间戳 |
format |
string |
❌ |
- |
可选的格式模板,用于指定解析格式 |
返回值:
-
类型:
Date | null
-
说明:解析成功返回 Date 对象,失败返回
null
4.2 支持的日期格式
4.2.1 自动识别格式(无需指定 format)
| 格式 |
示例 |
说明 |
| ISO 8601 |
2025-10-28T14:30:45.123Z |
JavaScript 原生支持 |
| 标准格式(带时分秒毫秒) |
2025-10-28 14:30:45.123 |
常用格式 |
| 标准格式(带时分秒) |
2025-10-28 14:30:45 |
常用格式 |
| 标准格式(带时分) |
2025-10-28 14:30 |
常用格式 |
| 标准日期 |
2025-10-28 |
仅日期 |
| 斜杠格式(带时间) |
2025/10/28 14:30:45 |
常用格式 |
| 斜杠格式(日期) |
2025/10/28 |
常用格式 |
| 中文格式(带时间) |
2025年10月28日 14时30分45秒 |
中文日期时间 |
| 中文格式(日期) |
2025年10月28日 |
中文日期 |
| 欧洲格式 |
28/10/2025 |
DD/MM/YYYY |
| 美式格式 |
10-28-2025 |
MM-DD-YYYY |
| 时间戳 |
1698484245000 |
毫秒时间戳 |
4.2.2 指定格式解析
当自动识别失败时,可以指定 format 参数明确告知解析格式:
const tz = new SurveyTimezone();
// 指定格式解析
tz.parse('28-10-2025', 'DD-MM-YYYY');
tz.parse('10/28/2025', 'MM/DD/YYYY');
tz.parse('25/10/28 14:30', 'YY/MM/DD HH:mm');
支持的格式标记:
-
YYYY / yyyy - 四位年份
-
YY / yy - 两位年份(00-49 → 2000-2049,50-99 → 1950-1999)
-
MM - 月份
-
DD / dd - 日期
-
HH - 小时
-
mm - 分钟
-
ss - 秒
-
SSS - 毫秒
4.3 使用示例
4.3.1 基础用法(自动识别)
const tz = new SurveyTimezone();
// 标准格式
tz.parse('2025-10-28 14:30:45'); // Date 对象
tz.parse('2025-10-28'); // Date 对象
// 斜杠格式
tz.parse('2025/10/28 14:30:45'); // Date 对象
tz.parse('2025/10/28'); // Date 对象
// 中文格式
tz.parse('2025年10月28日'); // Date 对象
tz.parse('2025年10月28日 14时30分45秒'); // Date 对象
// 时间戳
tz.parse(1698484245000); // Date 对象
// 带毫秒
tz.parse('2025-10-28 14:30:45.123'); // Date 对象
4.3.2 指定格式解析
const tz = new SurveyTimezone();
// 欧洲日期格式(DD/MM/YYYY)
tz.parse('28/10/2025', 'DD/MM/YYYY');
// 美式日期格式(MM/DD/YYYY)
tz.parse('10/28/2025', 'MM/DD/YYYY');
// 短年份格式
tz.parse('25/10/28', 'YY/MM/DD'); // 2025-10-28
// 自定义格式
tz.parse('28-10-2025 14:30', 'DD-MM-YYYY HH:mm');
4.3.3 错误处理
const tz = new SurveyTimezone();
// 无效输入返回 null
tz.parse(null); // null
tz.parse(undefined); // null
tz.parse(''); // null
tz.parse('invalid date'); // null
tz.parse('2025-13-40'); // null(无效日期)
// 类型检查
const result = tz.parse('2025-10-28');
if (result) {
console.log('解析成功:', result);
} else {
console.log('解析失败');
}
4.3.4 静态方法调用
// 直接使用静态方法,无需实例化
const date1 = SurveyTimezone.parse('2025-10-28');
const date2 = SurveyTimezone.parse('2025/10/28');
const date3 = SurveyTimezone.parse('2025年10月28日');
const date4 = SurveyTimezone.parse('28-10-2025', 'DD-MM-YYYY');
4.3.5 parse + format 组合使用
const tz = new SurveyTimezone();
// 解析后格式化输出
const parsedDate = tz.parse('2025-10-28 14:30:45');
if (parsedDate) {
console.log(tz.format(parsedDate, 'YYYY年MM月DD日')); // '2025年10月28日'
console.log(tz.format(parsedDate, 'MMM DD, yyyy')); // 'Oct 28, 2025'
console.log(tz.format(parsedDate, 'HH:mm:ss')); // '14:30:45'
}
// 格式转换
const input = '28/10/2025';
const date = tz.parse(input, 'DD/MM/YYYY');
const output = tz.format(date, 'YYYY-MM-DD');
console.log(output); // '2025-10-28'
4.4 解析流程
parse 方法采用多层次解析策略:
输入值
↓
┌─────────────────────────────────┐
│ 1. 类型检查 │
│ - null/undefined → null │
│ - Date 对象 → 验证后返回 │
│ - 数字 → 时间戳解析 │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 2. 格式化参数检查 │
│ - 有 format → 使用格式模板解析 │
│ - 无 format → 自动识别 │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 3. 原生解析尝试 │
│ - new Date(dateString) │
│ - 成功 → 返回 │
│ - 失败 → 继续 │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 4. 正则匹配解析 │
│ - 遍历预定义格式列表 │
│ - 匹配成功 → 返回 │
│ - 全部失败 → 返回 null │
└─────────────────────────────────┘
4.5 常见场景示例
场景1:表单日期输入
const tz = new SurveyTimezone();
// 用户输入的日期字符串
const userInput = document.getElementById('dateInput').value; // '2025-10-28'
const date = tz.parse(userInput);
if (date) {
// 转换为显示格式
const displayText = tz.format(date, 'YYYY年MM月DD日');
console.log(displayText); // '2025年10月28日'
}
场景2:API 数据转换
const tz = new SurveyTimezone();
// API 返回的日期字符串
const apiData = {
createdAt: '2025-10-28T14:30:45.123Z',
updatedAt: '2025/10/28 14:30:45'
};
// 解析并格式化
const createdDate = tz.parse(apiData.createdAt);
const updatedDate = tz.parse(apiData.updatedAt);
console.log(tz.format(createdDate, 'YYYY-MM-DD HH:mm:ss'));
console.log(tz.format(updatedDate, 'YYYY-MM-DD HH:mm:ss'));
场景3:日期格式统一化
const tz = new SurveyTimezone();
// 不同格式的日期数组
const dates = [
'2025-10-28',
'2025/10/28',
'2025年10月28日',
'28/10/2025' // 需要指定格式
];
// 统一转换为标准格式
const normalized = dates.map((dateStr, index) => {
const format = index === 3 ? 'DD/MM/YYYY' : undefined;
const date = tz.parse(dateStr, format);
return date ? tz.format(date, 'YYYY-MM-DD') : null;
});
console.log(normalized); // ['2025-10-28', '2025-10-28', '2025-10-28', '2025-10-28']
4.6 性能考虑
最佳实践:
-
优先使用标准格式:ISO 8601 格式解析最快
-
指定格式模板:已知格式时指定 format 参数可跳过自动识别
-
缓存解析结果:避免重复解析相同字符串
-
提前验证:在解析前进行基本格式验证
const tz = new SurveyTimezone();
// ❌ 不推荐:每次都自动识别
for (let i = 0; i < 1000; i++) {
tz.parse('28/10/2025');
}
// ✅ 推荐:指定格式
for (let i = 0; i < 1000; i++) {
tz.parse('28/10/2025', 'DD/MM/YYYY');
}
4.7 与 format 方法的配合
parse 和 format 是互补的两个方法:
| 方法 |
输入 |
输出 |
用途 |
parse |
字符串 → Date |
将日期字符串转换为 Date 对象 |
数据输入、解析 |
format |
Date → 字符串 |
将 Date 对象转换为格式化字符串 |
数据显示、输出 |
完整的数据流:
const tz = new SurveyTimezone();
// 数据输入 → 处理 → 输出
const input = '28/10/2025'; // 用户输入
const date = tz.parse(input, 'DD/MM/YYYY'); // 解析为 Date 对象
const output = tz.format(date, 'YYYY年MM月DD日'); // 格式化为显示文本
console.log(output); // '2025年10月28日'
4.8 原型方法 vs 静态方法
| 特性 |
原型方法 |
静态方法 |
| 调用方式 |
instance.parse() |
SurveyTimezone.parse() |
| 是否需要实例化 |
✅ 需要 |
❌ 不需要 |
| 访问实例属性 |
✅ 可以 |
❌ 不可以 |
| 推荐使用场景 |
面向对象编程 |
工具函数调用 |
关系说明:
// 静态方法实现(调用原型方法)
SurveyTimezone.parse = function (dateString, format) {
// 使用原型对象作为上下文,以便访问内部方法
return SurveyTimezone.prototype.parse.call(SurveyTimezone.prototype, dateString, format);
}
技术说明:
静态方法 parse 内部调用原型方法时,需要使用 SurveyTimezone.prototype 作为上下文(this),而不是 null。这是因为原型方法中可能会调用其他内部方法(如 _parseWithFormat),如果 this 为 null 会导致错误。
源码
/**
* SurveyTimezone - 时间处理插件
* @description 专门用于处理调查问卷中的时区转换和显示问题
* @version 1.0.0
* @author wjxcom
*/
(function (global, factory) {
// UMD模式支持 - 兼容AMD、CommonJS和全局变量
if (typeof define === 'function' && define.amd) {
// AMD模式
define(function () { return factory(); });
} else if (typeof module === 'object' && module.exports) {
// CommonJS模式
module.exports = factory();
} else {
// 浏览器全局变量模式
global.SurveyTimezone = factory();
}
}(typeof window !== 'undefined' ? window : this, function () {
'use strict';
/**
* 单例实例存储
* @private
*/
var instance = null;
/**
* 时区数据映射表(用于快速查找)
* @private
*/
var timezoneDataMap = {};
/**
* SurveyTimezone 主类(单例模式)
* @param {string|number|Date|Object} input - 日期时间字符串、时间戳、Date对象或配置对象
* @param {string} format - 可选的日期格式(当 input 为字符串时使用)
* @returns {SurveyTimezone} 单例实例
* @description 采用单例模式,多次实例化返回同一个对象
* @example
* new SurveyTimezone('2025-10-28 14:30:45');
* new SurveyTimezone(1698484245000);
* new SurveyTimezone('28/10/2025', 'DD/MM/YYYY');
* new SurveyTimezone({ date: '2025-10-28', timezone: 'Asia/Shanghai' });
*/
function SurveyTimezone(input, format) {
// 单例模式:如果已存在实例,直接返回
if (instance) {
return instance;
}
// 确保通过 new 调用
if (!(this instanceof SurveyTimezone)) {
return new SurveyTimezone(input, format);
}
// 解析输入参数
this._parseInput(input, format);
// 保存单例实例
instance = this;
// 执行初始化
this._init();
return instance;
}
SurveyTimezone.prototype = {
constructor: SurveyTimezone,
version: '1.0.0',
/**
* 解析输入参数
* @private
* @param {string|number|Date|Object} input - 输入参数
* @param {string} format - 日期格式
*/
_parseInput: function (input, format) {
// 初始化配置对象
this.options = {};
this.date = null;
// 如果没有输入,使用当前时间
if (input === undefined || input === null) {
this.date = new Date();
return;
}
// 如果是配置对象
if (typeof input === 'object' && !(input instanceof Date)) {
this.options = input;
// 从配置中提取日期
if (input.date !== undefined) {
this.date = this.parse(input.date, input.format || format);
} else {
this.date = new Date();
}
return;
}
// 其他情况:字符串、数字、Date 对象
this.date = this.parse(input, format);
// 如果解析失败,使用当前时间
if (!this.date) {
this.date = new Date();
}
},
/**
* 初始化方法
* @private
*/
_init: function () {
// 初始化逻辑
// 可以在这里添加时区处理、本地化等逻辑
},
/**
* 获取当前实例的日期对象
* @returns {Date} 日期对象
*/
getDate: function () {
return this.date;
},
/**
* 设置日期
* @param {string|number|Date} dateInput - 日期时间字符串、时间戳或Date对象
* @param {string} format - 可选的日期格式
* @returns {SurveyTimezone} 返回当前实例(链式调用)
*/
setDate: function (dateInput, format) {
this.date = this.parse(dateInput, format);
if (!this.date) {
this.date = new Date();
}
return this;
},
/**
* 格式化日期 - 兼容 dayjs 和 laydate 的格式化方式(原型方法)
* @param {Date|string} date - 要格式化的日期对象(可选,默认使用实例日期)
* @param {string} format - 格式化模板字符串(默认:'YYYY-MM-DD HH:mm:ss')
* @returns {string} 格式化后的日期字符串
* @description 支持的格式化标记:
* 年份:
* YYYY/yyyy - 四位年份(2025)
* YY/y - 两位年份(25)
* 月份:
* MMM - 英文月份缩写(Jan, Feb, Mar...)
* MM - 两位月份(01-12)
* M - 月份(1-12)
* 日期:
* DD/dd - 两位日期(01-31)
* D/d - 日期(1-31)
* 时间:
* HH - 24小时制小时(00-23)
* H - 24小时制小时(0-23)
* mm - 分钟(00-59)
* m - 分钟(0-59)
* ss - 秒(00-59)
* s - 秒(0-59)
* 毫秒:
* SSS - 毫秒(000-999)
* @example
* const tz = new SurveyTimezone('2025-10-28 14:30:45');
* tz.format(); // '2025-10-28 14:30:45'(使用实例日期)
* tz.format('YYYY年MM月DD日'); // '2025年10月28日'(使用实例日期)
* tz.format(new Date(), 'YYYY-MM-DD'); // 格式化指定日期
*/
format: function (date, format) {
// 如果第一个参数是字符串,说明是格式参数
if (typeof date === 'string' && !format) {
format = date;
date = this.date;
}
// 如果没有传入 date,使用实例的日期
if (!date || typeof date === 'string') {
date = this.date;
}
// 默认格式
format = format || 'YYYY-MM-DD HH:mm:ss';
// 验证日期对象
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return '';
}
// 月份英文缩写
var monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// 定义格式化标记映射
var matches = {
// 年份(支持 dayjs 和 laydate 格式)
'YYYY': function() { return date.getFullYear(); },
'yyyy': function() { return date.getFullYear(); },
'YY': function() { return String(date.getFullYear()).slice(-2); },
'y': function() { return String(date.getFullYear()).slice(-2); },
// 月份
'MMM': function() { return monthNames[date.getMonth()]; },
'MM': function() { return ('0' + (date.getMonth() + 1)).slice(-2); },
'M': function() { return date.getMonth() + 1; },
// 日期(支持 dayjs 和 laydate 格式)
'DD': function() { return ('0' + date.getDate()).slice(-2); },
'dd': function() { return ('0' + date.getDate()).slice(-2); },
'D': function() { return date.getDate(); },
'd': function() { return date.getDate(); },
// 时间
'HH': function() { return ('0' + date.getHours()).slice(-2); },
'H': function() { return date.getHours(); },
'mm': function() { return ('0' + date.getMinutes()).slice(-2); },
'm': function() { return date.getMinutes(); },
'ss': function() { return ('0' + date.getSeconds()).slice(-2); },
's': function() { return date.getSeconds(); },
// 毫秒
'SSS': function() { return ('00' + date.getMilliseconds()).slice(-3); }
};
// 按标记长度从长到短排序,先处理长标记避免冲突
// 注意:MMM 要在 MM 之前处理,yyyy 要在 y 之前处理
var tokens = ['YYYY', 'yyyy', 'MMM', 'SSS', 'MM', 'DD', 'dd', 'HH', 'mm', 'ss', 'YY', 'M', 'D', 'd', 'H', 'm', 's', 'y'];
var result = format;
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (result.indexOf(token) !== -1) {
result = result.replace(new RegExp(token, 'g'), matches[token]());
}
}
return result;
},
/**
* 解析日期时间字符串(原型方法)
* @param {string|number} dateString - 日期时间字符串或时间戳
* @param {string} format - 可选的格式模板,用于指定解析格式
* @returns {Date|null} 解析后的 Date 对象,解析失败返回 null
* @description 支持多种常见日期格式的自动识别和解析
* @example
* const tz = new SurveyTimezone();
* tz.parse('2025-10-28 14:30:45'); // Date 对象
* tz.parse('2025/10/28'); // Date 对象
* tz.parse('2025年10月28日'); // Date 对象
* tz.parse(1698484245000); // Date 对象(时间戳)
* tz.parse('invalid'); // null
*/
parse: function (dateString, format) {
// 处理 null 或 undefined
if (dateString == null) {
return null;
}
// 如果已经是 Date 对象,直接返回
if (dateString instanceof Date) {
return isNaN(dateString.getTime()) ? null : dateString;
}
// 处理数字类型(时间戳)
if (typeof dateString === 'number') {
var date = new Date(dateString);
return isNaN(date.getTime()) ? null : date;
}
// 转换为字符串
dateString = String(dateString).trim();
if (!dateString) {
return null;
}
// 如果指定了格式模板,使用格式模板解析
if (format) {
return this._parseWithFormat(dateString, format);
}
// 尝试使用原生 Date 解析
var nativeDate = new Date(dateString);
if (!isNaN(nativeDate.getTime())) {
return nativeDate;
}
// 尝试常见格式的正则匹配
var patterns = [
// YYYY-MM-DD HH:mm:ss.SSS
{
regex: /^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})\.(\d{1,3})$/,
handler: function(m) {
return new Date(m[1], m[2] - 1, m[3], m[4], m[5], m[6], m[7]);
}
},
// YYYY-MM-DD HH:mm:ss
{
regex: /^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})$/,
handler: function(m) {
return new Date(m[1], m[2] - 1, m[3], m[4], m[5], m[6]);
}
},
// YYYY-MM-DD HH:mm
{
regex: /^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/,
handler: function(m) {
return new Date(m[1], m[2] - 1, m[3], m[4], m[5]);
}
},
// YYYY-MM-DD
{
regex: /^(\d{4})-(\d{1,2})-(\d{1,2})$/,
handler: function(m) {
return new Date(m[1], m[2] - 1, m[3]);
}
},
// YYYY/MM/DD HH:mm:ss
{
regex: /^(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})$/,
handler: function(m) {
return new Date(m[1], m[2] - 1, m[3], m[4], m[5], m[6]);
}
},
// YYYY/MM/DD
{
regex: /^(\d{4})\/(\d{1,2})\/(\d{1,2})$/,
handler: function(m) {
return new Date(m[1], m[2] - 1, m[3]);
}
},
// YYYY年MM月DD日 HH时mm分ss秒
{
regex: /^(\d{4})年(\d{1,2})月(\d{1,2})日\s*(\d{1,2})时(\d{1,2})分(\d{1,2})秒$/,
handler: function(m) {
return new Date(m[1], m[2] - 1, m[3], m[4], m[5], m[6]);
}
},
// YYYY年MM月DD日
{
regex: /^(\d{4})年(\d{1,2})月(\d{1,2})日$/,
handler: function(m) {
return new Date(m[1], m[2] - 1, m[3]);
}
},
// DD/MM/YYYY
{
regex: /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/,
handler: function(m) {
return new Date(m[3], m[2] - 1, m[1]);
}
},
// MM/DD/YYYY (美式格式)
{
regex: /^(\d{1,2})-(\d{1,2})-(\d{4})$/,
handler: function(m) {
return new Date(m[3], m[1] - 1, m[2]);
}
}
];
// 尝试匹配各种格式
for (var i = 0; i < patterns.length; i++) {
var match = dateString.match(patterns[i].regex);
if (match) {
var parsedDate = patterns[i].handler(match);
if (!isNaN(parsedDate.getTime())) {
return parsedDate;
}
}
}
// 解析失败
return null;
},
/**
* 使用指定格式解析日期字符串(内部方法)
* @private
* @param {string} dateString - 日期字符串
* @param {string} format - 格式模板
* @returns {Date|null} 解析后的 Date 对象
*/
_parseWithFormat: function (dateString, format) {
// 构建正则表达式,将格式标记替换为捕获组
var formatRegex = format
.replace(/YYYY|yyyy/g, '(\\d{4})')
.replace(/YY|yy/g, '(\\d{2})')
.replace(/MM/g, '(\\d{1,2})')
.replace(/DD|dd/g, '(\\d{1,2})')
.replace(/HH/g, '(\\d{1,2})')
.replace(/mm/g, '(\\d{1,2})')
.replace(/ss/g, '(\\d{1,2})')
.replace(/SSS/g, '(\\d{1,3})');
var regex = new RegExp('^' + formatRegex + '$');
var match = dateString.match(regex);
if (!match) {
return null;
}
// 提取各个部分
var tokens = format.match(/YYYY|yyyy|YY|yy|MM|DD|dd|HH|mm|ss|SSS/g) || [];
var values = {
year: 0,
month: 0,
day: 1,
hour: 0,
minute: 0,
second: 0,
millisecond: 0
};
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
var value = parseInt(match[i + 1], 10);
if (token === 'YYYY' || token === 'yyyy') {
values.year = value;
} else if (token === 'YY' || token === 'yy') {
values.year = value < 50 ? 2000 + value : 1900 + value;
} else if (token === 'MM') {
values.month = value - 1;
} else if (token === 'DD' || token === 'dd') {
values.day = value;
} else if (token === 'HH') {
values.hour = value;
} else if (token === 'mm') {
values.minute = value;
} else if (token === 'ss') {
values.second = value;
} else if (token === 'SSS') {
values.millisecond = value;
}
}
var date = new Date(
values.year,
values.month,
values.day,
values.hour,
values.minute,
values.second,
values.millisecond
);
return isNaN(date.getTime()) ? null : date;
}
}
/**
* 获取单例实例(静态方法)
* @param {Object} options - 配置选项
* @returns {SurveyTimezone} 单例实例
*/
SurveyTimezone.getInstance = function (options) {
if (!instance) {
instance = new SurveyTimezone(options);
}
return instance;
}
/**
* 重置单例实例(用于测试或重新初始化)
* @static
*/
SurveyTimezone.resetInstance = function () {
instance = null;
}
// ==================== 静态方法 ====================
/**
* 格式化日期 - 静态方法(向后兼容)
* @static
* @param {Date} date - 要格式化的日期对象
* @param {string} format - 格式化模板字符串
* @returns {string} 格式化后的日期字符串
* @description 静态方法,可直接调用而无需实例化
* @example
* SurveyTimezone.format(new Date(), 'YYYY-MM-DD');
*/
SurveyTimezone.format = function (date, format) {
// 调用原型方法实现
return SurveyTimezone.prototype.format.call(null, date, format);
}
/**
* 解析日期时间字符串 - 静态方法(向后兼容)
* @static
* @param {string|number} dateString - 日期时间字符串或时间戳
* @param {string} format - 可选的格式模板
* @returns {Date|null} 解析后的 Date 对象,解析失败返回 null
* @description 静态方法,可直接调用而无需实例化
* @example
* SurveyTimezone.parse('2025-10-28 14:30:45');
* SurveyTimezone.parse('2025/10/28');
* SurveyTimezone.parse('28/10/2025', 'DD/MM/YYYY');
*/
SurveyTimezone.parse = function (dateString, format) {
// 使用原型对象作为上下文调用原型方法
return SurveyTimezone.prototype.parse.call(SurveyTimezone.prototype, dateString, format);
}
// 返回构造函数
return SurveyTimezone;
}))
测试文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="index.js"></script>
</head>
<body>
<script>
console.log('=== 初始化测试 - 传入日期时间 ===');
// 重置单例以便测试
SurveyTimezone.resetInstance();
// 方式1: 传入日期字符串
const tz1 = new SurveyTimezone('2025-10-28 14:30:45');
console.log('字符串初始化:', tz1.getDate());
console.log('格式化输出:', tz1.format());
console.log('自定义格式:', tz1.format('YYYY年MM月DD日 HH时mm分'));
// 重置单例
SurveyTimezone.resetInstance();
// 方式2: 传入时间戳
const tz2 = new SurveyTimezone(1698484245000);
console.log('\n时间戳初始化:', tz2.getDate());
console.log('格式化输出:', tz2.format());
// 重置单例
SurveyTimezone.resetInstance();
// 方式3: 传入日期字符串和格式
const tz3 = new SurveyTimezone('28/10/2025', 'DD/MM/YYYY');
console.log('\n指定格式初始化:', tz3.getDate());
console.log('格式化输出:', tz3.format('YYYY-MM-DD'));
// 重置单例
SurveyTimezone.resetInstance();
// 方式4: 传入配置对象
const tz4 = new SurveyTimezone({
date: '2025-10-28 14:30:45',
timezone: 'Asia/Shanghai'
});
console.log('\n配置对象初始化:', tz4.getDate());
console.log('格式化输出:', tz4.format());
console.log('配置信息:', tz4.options);
// 重置单例
SurveyTimezone.resetInstance();
// 方式5: 不传参数(使用当前时间)
const tz5 = new SurveyTimezone();
console.log('\n默认初始化(当前时间):', tz5.getDate());
console.log('格式化输出:', tz5.format());
console.log('\n=== setDate 方法测试 ===');
// 修改日期
tz5.setDate('2025-12-25 00:00:00');
console.log('修改后的日期:', tz5.getDate());
console.log('格式化输出:', tz5.format('YYYY年MM月DD日'));
// 链式调用
console.log('链式调用:', tz5.setDate('2026-01-01').format('YYYY/MM/DD'));
console.log('\n=== 单例模式验证 ===');
// 验证单例
const instance1 = tz5;
const instance2 = new SurveyTimezone('2025-10-28');
console.log('单例验证:', instance1 === instance2 ? '✓ 通过(返回同一实例)' : '✗ 失败');
console.log('\n=== 格式化方法测试 ===');
// 格式化实例日期
SurveyTimezone.resetInstance();
const tzFormat = new SurveyTimezone('2025-10-28 14:30:45');
console.log('格式化实例日期(无参):', tzFormat.format());
console.log('格式化实例日期(指定格式):', tzFormat.format('YYYY年MM月DD日'));
console.log('格式化实例日期(英文):', tzFormat.format('MMM DD, yyyy'));
// 格式化指定日期
const now = new Date();
console.log('格式化指定日期:', tzFormat.format(now, 'YYYY-MM-DD HH:mm:ss'));
console.log('\n=== 格式化方法测试 - 静态方法(向后兼容)===');
console.log('静态方法 - 完整格式:', SurveyTimezone.format(now, 'YYYY-MM-DD HH:mm:ss'));
console.log('静态方法 - laydate格式:', SurveyTimezone.format(now, 'yyyy-MM-dd HH:mm:ss'));
console.log('静态方法 - 英文月份:', SurveyTimezone.format(now, 'MMM DD, yyyy'));
console.log('静态方法 - 自定义格式:', SurveyTimezone.format(now, 'YYYY年MM月DD日 HH时mm分ss秒'));
console.log('\n=== parse 方法测试(静态方法)===');
// 测试静态方法 - 自动识别格式
console.log('parse标准格式:', SurveyTimezone.parse('2025-10-28 14:30:45'));
console.log('parse中文格式:', SurveyTimezone.parse('2025年10月28日'));
console.log('parse时间戳:', SurveyTimezone.parse(1698484245000));
// 测试静态方法 - 指定格式(这个会触发 _parseWithFormat)
console.log('parse指定格式1:', SurveyTimezone.parse('28/10/2025', 'DD/MM/YYYY'));
console.log('parse指定格式2:', SurveyTimezone.parse('2025年10月28日', 'YYYY年MM月DD日'));
console.log('parse指定格式3:', SurveyTimezone.parse('25-10-28', 'YY-MM-DD'));
console.log('\n=== 完整工作流测试 ===');
// 场景:用户输入 → 解析 → 处理 → 格式化输出
SurveyTimezone.resetInstance();
const workflow = new SurveyTimezone('28/10/2025', 'DD/MM/YYYY');
console.log('输入:', '28/10/2025');
console.log('解析:', workflow.getDate());
console.log('输出1:', workflow.format('YYYY-MM-DD'));
console.log('输出2:', workflow.format('YYYY年MM月DD日'));
console.log('输出3:', workflow.format('MMM DD, yyyy'));
</script>
</body>
</html>