javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript时间处理插件

基于JavaScript优雅的实现一个时间处理插件

作者:会联营的陆逊

这篇文章主要介绍了如何优雅地实现一个时间处理插件,包括UMD模式、单例模式的实现,以及插件的初始化、方法(如getDate、setDate、format、parse)的定义和使用,文章还涵盖了格式化标记、解析流程、常见场景和性能优化建议,需要的朋友可以参考下

1. UMD 模式解析

1.1 立即执行函数 (IIFE)

外层是一个立即执行函数,接收 globalfactory 两个参数。

global 参数说明:

typeof window !== "undefined" ? window : this;

根据不同的运行环境,global 参数会指向:

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();
}

支持的模块系统:

1.3 工厂函数解析

工厂函数 factory 返回插件的构造函数:

function () {
    'use strict';

    // 构造函数定义
    function SurveyTimezone(options) {
    }

    // 原型方法定义
    SurveyTimezone.prototype = {
        constructor: SurveyTimezone, // 修复 constructor 指向
        version: '1.0.0',
        _init: function () {
        }
    }

    // 返回构造函数
    return SurveyTimezone;
}

核心组成部分:

1.4 执行流程

UMD 模块的加载和执行流程:

  1. 立即执行:代码加载后立即执行 IIFE
  2. 环境检测:检测当前支持的模块系统(AMD / CommonJS / 全局变量)
  3. 工厂调用:执行 factory() 函数,返回 SurveyTimezone 构造函数
  4. 模块导出:根据环境将构造函数导出到相应位置
(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;
}

实现要点:

  1. 实例检查:首次检查是否已存在实例,有则直接返回
  2. new 检查:确保即使不用 new 关键字也能正常工作
  3. 配置初始化:保存传入的配置选项
  4. 实例保存:将当前实例保存到闭包变量中
  5. 初始化执行:调用内部初始化方法

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)

参数说明:

参数类型必填说明
inputstring|number|Date|Object日期时间字符串、时间戳、Date对象或配置对象
formatstring日期格式(当 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 方法用于格式化日期对象,兼容 dayjslaydate 两种流行的日期格式化风格。

方法签名:

// 原型方法
instance.format(date, format)

// 静态方法(向后兼容)
SurveyTimezone.format(date, format)

参数:

参数类型必填默认值说明
dateDate-要格式化的 JavaScript Date 对象
formatstring'YYYY-MM-DD HH:mm:ss'格式化模板字符串

返回值:

3.2 支持的格式化标记

3.2.1 年份标记

标记说明示例输出兼容性
YYYY四位年份2025dayjs
yyyy四位年份2025laydate
YY两位年份25dayjs
y两位年份25laydate

3.2.2 月份标记

标记说明示例输出兼容性
MMM英文月份缩写Jan, Feb, Mar...通用
MM两位月份(补零)01, 02... 12通用
M月份(不补零)1, 2... 12通用

3.2.3 日期标记

标记说明示例输出兼容性
DD两位日期(补零)01, 02... 31dayjs
dd两位日期(补零)01, 02... 31laydate
D日期(不补零)1, 2... 31dayjs
d日期(不补零)1, 2... 31laydate

3.2.4 时间标记

标记说明示例输出兼容性
HH24小时制小时(补零)00, 01... 23通用
H24小时制小时(不补零)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... 999dayjs

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-DD2025-10-28标准日期格式
YYYY-MM-DD HH:mm:ss2025-10-28 14:30:45完整日期时间
yyyy年MM月dd日2025年10月28日中文日期
MMM DD, yyyyOct 28, 2025英文日期
YYYY/MM/DD HH:mm2025/10/28 14:30简短日期时间
HH:mm:ss14:30:45仅时间
YY-M-D25-10-28简短日期
YYYY-MM-DD HH:mm:ss.SSS2025-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 方法采用正则替换策略实现格式化:

  1. 标记解析:将格式字符串中的标记(如 YYYYMM)识别出来
  2. 长度优先:按标记长度从长到短处理,避免 YYYYYY 误替换
  3. 顺序处理:依次替换每个标记为对应的日期值
  4. 类型转换:使用 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)

参数:

参数类型必填默认值说明
dateStringstring|number-日期时间字符串或时间戳
formatstring-可选的格式模板,用于指定解析格式

返回值:

4.2 支持的日期格式

4.2.1 自动识别格式(无需指定 format)

格式示例说明
ISO 86012025-10-28T14:30:45.123ZJavaScript 原生支持
标准格式(带时分秒毫秒)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/2025DD/MM/YYYY
美式格式10-28-2025MM-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');

支持的格式标记:

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 性能考虑

最佳实践:

  1. 优先使用标准格式:ISO 8601 格式解析最快
  2. 指定格式模板:已知格式时指定 format 参数可跳过自动识别
  3. 缓存解析结果:避免重复解析相同字符串
  4. 提前验证:在解析前进行基本格式验证
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 方法的配合

parseformat 是互补的两个方法:

方法输入输出用途
parse字符串 → Date将日期字符串转换为 Date 对象数据输入、解析
formatDate → 字符串将 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),如果 thisnull 会导致错误。

源码

/**
 * 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>

以上就是基于JavaScript优雅的实现一个时间处理插件的详细内容,更多关于JavaScript时间处理插件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文