0%

Moment.JS 使用示例——日期时间处理完整指南

虽然现在推荐使用 dayjs 替代 moment.JS,但工作中还是经常会遇到使用 moment.JS 的老项目。顺便整理了一下 moment.JS 的常用方法和一些经典案例,帮助大家快速上手和维护老项目中的日期处理代码。

Moment.JS 简介

  Moment.JS 是一个流行的 Javascript 日期处理库,提供了丰富的 API 来解析、验证、操作和显示日期和时间。尽管官方已宣布不再积极开发(但仍会维护 bug 修复),但在许多现有项目中仍有广泛应用。

  Moment.JS 的主要特点:

  1. 丰富的 API: 提供了日期解析、格式化、计算、比较等功能
  2. 链式调用: 支持方法链,使代码更具可读性
  3. 国际化支持: 支持多种语言和地区的日期格式
  4. 时区处理: 通过插件支持时区转换
  5. 兼容性好: 支持 IE9+等老旧浏览器

⚠️ 重要提醒: Moment.JS 官方已宣布不推荐在新项目中使用,请考虑使用 dayjs、date-fns 或其他现代替代方案。

安装和基本使用

1
2
3
4
5
6
# npm 安装 npm install moment

# yarn 安装 yarn add moment

# CDN 引入
# <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.JS/2.29.4/moment.min.JS"></script>
1
2
3
4
5
6
7
8
// ES6导入 import moment from 'moment';
import 'moment/locale/zh-cn'; // 中文本地化

// CommonJS 导入 const moment = require('moment');

// 基本使用 const now = moment(); // 当前时间 console.log(now.format()); // ISO 8601格式 console.log(now.format('YYYY-MM-DD HH:mm:ss')); // 自定义格式

// 设置本地化 moment.locale('zh-cn');

核心功能详解

1. 日期解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 多种方式创建 moment 对象 const now = moment(); // 当前时间 const fromString = moment('2024-01-01'); // 字符串日期 const fromTimestamp = moment(1704067200000); // 时间戳 const fromJsDate = moment(new Date()); // JS Date 对象 const fromDateArray = moment([2024, 0, 1]); // 数组格式 [年, 月-1, 日]

// 指定格式解析 const customParsed = moment('01-01-2024', 'MM-DD-YYYY');
console.log(customParsed.format('YYYY-MM-DD')); // 2024-01-01

// 解析 ISO 8601格式 const isoDate = moment('2024-01-01T12:00:00.000Z');
console.log(isoDate.format()); // 2024-01-01T12:00:00+00:00

// 从对象创建 const fromObject = moment({
year: 2024,
month: 2, // 0-11 (3月)
date: 2024-05-12 10:00:00
hour: 14,
minute: 30,
second: 0
});
console.log(fromObject.format('YYYY-MM-DD HH:mm:ss')); // 2024-03-15 14:30:00

// 解析相对时间 const weekAgo = moment().subtract(1, 'week');
const monthLater = moment().add(1, 'month');

2. 日期格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 常用格式化选项 const date = moment('2024-03-15T14:30:00');

// 基本格式化 console.log(date.format('YYYY')); // 年份: 2024
console.log(date.format('MM')); // 月份: 03
console.log(date.format('DD')); // 日期: 15
console.log(date.format('HH')); // 小时: 14
console.log(date.format('mm')); // 分钟: 30
console.log(date.format('ss')); // 秒: 00

// 组合格式 console.log(date.format('YYYY-MM-DD')); // 2024-03-15
console.log(date.format('YYYY-MM-DD HH:mm:ss')); // 2024-03-15 14:30:00
console.log(date.format('MMM DD, YYYY')); // Mar 15, 2024
console.log(date.format('dddd, MMMM D, YYYY')); // Friday, March 15, 2024

// 本地化格式 moment.locale('zh-cn');
console.log(date.format('LLLL')); // 2024年3月15日星期五 下午2点30分 console.log(date.format('LT')); // 下午2:30
console.log(date.format('LTS')); // 下午2:30:00
console.log(date.format('L')); // 2024-03-15
console.log(date.format('LL')); // 2024年3月15日 console.log(date.format('LLL')); // 2024年3月15日下午2点30分 console.log(date.format('LLLL')); // 2024年3月15日星期五 下午2点30分

// 自定义格式化函数 function customFormat(date) {
const now = moment();
const diff = now.diff(date, 'days');

if (diff === 0) {
return '今天 ' + date.format('HH:mm');
} else if (diff === 1) {
return '昨天 ' + date.format('HH:mm');
} else if (diff < 7) {
return diff + '天前';
} else {
return date.format('MM-DD');
}
}

3. 日期计算和修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 日期加减运算 const baseDate = moment('2024-03-15');

console.log(baseDate.add(1, 'day').format('YYYY-MM-DD')); // 2024-03-16
console.log(baseDate.subtract(1, 'month').format('YYYY-MM-DD')); // 2024-02-15
console.log(baseDate.add(2, 'years').format('YYYY-MM-DD')); // 2026-03-15
console.log(baseDate.add(24, 'hours').format('YYYY-MM-DD HH:mm')); // 2024-03-16 00:00

// 链式调用 const complexDate = moment().add(1, 'year').add(2, 'months').subtract(3, 'days').add(5, 'hours');
console.log(complexDate.format('YYYY-MM-DD HH:mm:ss'));

// 支持多种单位 const now = moment();
console.log(now.add(1, 'week').format('YYYY-MM-DD')); // 一周后 console.log(now.subtract(30, 'minutes').format('YYYY-MM-DD HH:mm')); // 30分钟前 console.log(now.add(1, 'quarter').format('YYYY-MM-DD')); // 一季度后 console.log(now.add(100, 'days').format('YYYY-MM-DD')); // 100天后

// 直接修改日期部分 const date = moment('2024-03-15');
date.year(2025);
date.month(5); // 6月 (0-11)
date.date(20); // 20日 date.hour(18);
date.minute(30);
date.second(0);

console.log(date.format('YYYY-MM-DD HH:mm:ss')); // 2025-06-20 18:30:00

// 使用 set 方法 const date2 = moment('2024-03-15');
date2.set({
year: 2025,
month: 5, // 6月 date: 2024-05-12 10:00:00
hour: 18,
minute: 30
});

console.log(date2.format('YYYY-MM-DD HH:mm:ss')); // 2025-06-20 18:30:00

4. 日期比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 日期比较方法 const date1 = moment('2024-03-15');
const date2 = moment('2024-03-20');

console.log(date1.isBefore(date2)); // true
console.log(date1.isAfter(date2)); // false
console.log(date1.isSame(date2, 'day')); // false
console.log(date1.isSame(date1, 'day')); // true

// 比较到指定精度 console.log(date1.isSame(moment('2024-03-15T10:00:00'), 'day')); // true
console.log(date1.isSame(moment('2024-03-15T10:00:00'), 'hour')); // false

// 范围比较 const start = moment('2024-03-01');
const end = moment('2024-03-31');
const checkDate = moment('2024-03-15');

console.log(checkDate.isBetween(start, end, 'day', '[]')); // true (包含边界)

// 最大最小值比较 const dates = [
moment('2024-01-01'),
moment('2024-06-15'),
moment('2024-03-10')
];

const earliest = moment.min(dates);
const latest = moment.max(dates);

console.log(earliest.format('YYYY-MM-DD')); // 2024-01-01
console.log(latest.format('YYYY-MM-DD')); // 2024-06-15

高级功能

相对时间处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 相对时间格式化 const now = moment();
const past = now.clone().subtract(5, 'minutes');
const future = now.clone().add(1, 'hour');

console.log(past.fromNow()); // 5 minutes ago (英文) 或 几分钟前 (中文)
console.log(future.fromNow()); // in an hour 或 1 小时内 console.log(now.from(past)); // 5 minutes ago
console.log(now.to(future)); // in an hour

// 本地化相对时间配置 moment.updateLocale('zh-cn', {
relativeTime: {
future: '%s 内',
past: '%s 前',
s: '几秒',
ss: '%d 秒',
m: '1分钟',
mm: '%d 分钟',
h: '1小时',
hh: '%d 小时',
d: '1天',
dd: '%d 天',
w: '1周',
ww: '%d 周',
M: '1个月',
MM: '%d 个月',
y: '1年',
yy: '%d 年'
}
});

// 日历时间 const today = moment();
const yesterday = moment().subtract(1, 'day');
const tomorrow = moment().add(1, 'day');
const lastWeek = moment().subtract(1, 'week');
const nextWeek = moment().add(1, 'week');

console.log(today.calendar()); // Today at 3:30 PM
console.log(yesterday.calendar()); // Yesterday at 3:30 PM
console.log(tomorrow.calendar()); // Tomorrow at 3:30 PM
console.log(lastWeek.calendar()); // Last Monday at 3:30 PM
console.log(nextWeek.calendar()); // 04/16/2024

日期范围和区间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 创建日期范围工具 class DateRange {
constructor(startDate, endDate) {
this.start = moment(startDate);
this.end = moment(endDate);
}

// 获取区间内的天数 getDays() {
return this.end.diff(this.start, 'days') + 1;
}

// 获取区间内的所有日期 getAllDates() {
const dates = [];
let current = this.start.clone();

while (current.isSameOrBefore(this.end, 'day')) {
dates.push(current.clone());
current.add(1, 'day');
}

return dates;
}

// 获取工作日数量 getBusinessDays() {
let count = 0;
let current = this.start.clone();

while (current.isSameOrBefore(this.end, 'day')) {
const dayOfWeek = current.day();
if (dayOfWeek !== 0 && dayOfWeek !== 6) { // 非周末 count++;
}
current.add(1, 'day');
}

return count;
}

// 检查日期是否在范围内 contains(date) {
const d = moment(date);
return d.isBetween(this.start, this.end, 'day', '[]');
}

// 获取周数 getWeeks() {
return this.end.diff(this.start, 'weeks') + 1;
}

// 获取月数 getMonths() {
return this.end.diff(this.start, 'months') + 1;
}
}

// 使用示例 const range = new DateRange('2024-03-01', '2024-03-31');
console.log(range.getDays()); // 31
console.log(range.getBusinessDays()); // 23 (假设3月1日是周五)
console.log(range.contains('2024-03-15')); // true

时区处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 需要安装 moment-timezone 插件
// npm install moment-timezone
import moment from 'moment-timezone';

// 获取不同时区的时间 const beijingTime = moment().tz('Asia/Shanghai');
const newYorkTime = moment().tz('America/New_York');
const londonTime = moment().tz('Europe/London');
const tokyoTime = moment().tz('Asia/Tokyo');

console.log('北京时间:', beijingTime.format('YYYY-MM-DD HH:mm:ss Z'));
console.log('纽约时间:', newYorkTime.format('YYYY-MM-DD HH:mm:ss Z'));
console.log('伦敦时间:', londonTime.format('YYYY-MM-DD HH:mm:ss Z'));
console.log('东京时间:', tokyoTime.format('YYYY-MM-DD HH:mm:ss Z'));

// 转换时区 const utcTime = moment.utc('2024-01-01 12:00:00');
const localTime = utcTime.tz('Asia/Shanghai');
console.log('UTC 转上海时间:', localTime.format('YYYY-MM-DD HH:mm:ss Z'));

// 获取时区信息 console.log('上海时区偏移:', moment().tz('Asia/Shanghai').utcOffset());
console.log('纽约时区偏移:', moment().tz('America/New_York').utcOffset());

// 从特定时区创建时间 const shanghaiTime = moment.tz('2024-01-01 12:00:00', 'Asia/Shanghai');
const utcFromShanghai = shanghaiTime.clone().utc();
console.log('上海时间的 UTC 表示:', utcFromShanghai.format('YYYY-MM-DD HH:mm:ss Z'));

实际应用场景

场景1: 表单日期验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 日期验证工具类 class DateValidator {
static isValidDate(dateString, format = 'YYYY-MM-DD') {
const date = moment(dateString, format, true);
return date.isValid();
}

static isFutureDate(dateString) {
const date = moment(dateString);
return date.isAfter(moment(), 'day');
}

static isPastDate(dateString) {
const date = moment(dateString);
return date.isBefore(moment(), 'day');
}

static isInRange(dateString, startDate, endDate) {
const date = moment(dateString);
const start = moment(startDate);
const end = moment(endDate);

return date.isBetween(start, end, 'day', '[]');
}

static formatDateForDisplay(dateString, locale = 'zh-cn') {
moment.locale(locale);
const date = moment(dateString);
return date.format('YYYY 年 MM 月 DD 日');
}

static calculateAge(birthDateString) {
const birthDate = moment(birthDateString);
const today = moment();
return today.diff(birthDate, 'years');
}

static getDaysDifference(startDate, endDate) {
const start = moment(startDate);
const end = moment(endDate);
return end.diff(start, 'days');
}

static validateBirthDate(birthDateString, minAge = 0, maxAge = 120) {
if (!this.isValidDate(birthDateString)) {
return { valid: false, message: '日期格式不正确' };
}

const age = this.calculateAge(birthDateString);
if (age < minAge) {
return { valid: false, message: `年龄不能小于${minAge}岁` };
}

if (age > maxAge) {
return { valid: false, message: `年龄不能大于${maxAge}岁` };
}

return { valid: true, age };
}
}

// 使用示例 const formData = {
birthDate: '1990-05-15',
startDate: '2024-03-01',
endDate: '2024-12-31'
};

console.log(DateValidator.isValidDate(formData.birthDate)); // true
console.log(DateValidator.calculateAge(formData.birthDate)); // 33 (截至2024年)
console.log(DateValidator.isInRange('2024-06-15', formData.startDate, formData.endDate)); // true
console.log(DateValidator.validateBirthDate(formData.birthDate)); // { valid: true, age: 33 }

场景2: 日历组件数据生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// 月历数据生成器 class CalendarGenerator {
constructor(year, month) {
this.year = year;
this.month = month;
this.firstDayOfMonth = moment([year, month, 1]);
this.lastDayOfMonth = moment([year, month, 1]).endOf('month');
this.firstDayOfWeek = this.firstDayOfMonth.day(); // 0-6 (周日-周六)
}

// 获取月历数据 getCalendarData() {
const days = [];
const totalCells = 42; // 6周 × 7天

// 计算上个月需要显示的天数 for (let i = 0; i < this.firstDayOfWeek; i++) {
const day = this.firstDayOfMonth.clone().subtract(this.firstDayOfWeek - i, 'days');
days.push({
date: 2024-05-12 10:00:00
day: day.date(),
month: day.month(),
year: day.year(),
isCurrentMonth: false,
isToday: day.isSame(moment(), 'day'),
isWeekend: day.day() === 0 || day.day() === 6
});
}

// 当前月的天数 const daysInMonth = this.lastDayOfMonth.date();
for (let i = 1; i <= daysInMonth; i++) {
const day = moment([this.year, this.month, i]);
days.push({
date: 2024-05-12 10:00:00
day: i,
month: this.month,
year: this.year,
isCurrentMonth: true,
isToday: day.isSame(moment(), 'day'),
isWeekend: day.day() === 0 || day.day() === 6
});
}

// 下个月需要显示的天数 const remainingCells = totalCells - days.length;
for (let i = 1; i <= remainingCells; i++) {
const day = this.lastDayOfMonth.clone().add(i, 'days');
days.push({
date: 2024-05-12 10:00:00
day: day.date(),
month: day.month(),
year: day.year(),
isCurrentMonth: false,
isToday: day.isSame(moment(), 'day'),
isWeekend: day.day() === 0 || day.day() === 6
});
}

// 按周分组 const weeks = [];
for (let i = 0; i < days.length; i += 7) {
weeks.push(days.slice(i, i + 7));
}

return {
month: this.month,
year: this.year,
weeks,
firstDayOfMonth: this.firstDayOfMonth,
lastDayOfMonth: this.lastDayOfMonth
};
}

// 获取工作日列表 getBusinessDays() {
const businessDays = [];
let current = this.firstDayOfMonth.clone();

while (current.isSameOrBefore(this.lastDayOfMonth, 'day')) {
const dayOfWeek = current.day();
if (dayOfWeek !== 0 && dayOfWeek !== 6) { // 非周末 businessDays.push({
date: 2024-05-12 10:00:00
day: current.date(),
isToday: current.isSame(moment(), 'day')
});
}
current.add(1, 'day');
}

return businessDays;
}

// 获取月度统计数据 getMonthlyStats() {
const totalDays = this.lastDayOfMonth.date();
const businessDays = this.getBusinessDays().length;
const weekendDays = totalDays - businessDays;

return {
totalDays,
businessDays,
weekendDays,
weeksCount: Math.ceil((this.firstDayOfWeek + totalDays) / 7)
};
}
}

// 使用示例 const calendar = new CalendarGenerator(2024, 3); // 2024年4月 (month 是0-11)
console.log(calendar.getCalendarData());
console.log(calendar.getMonthlyStats());

场景3: 时间轴和历史记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// 时间轴生成器 class TimelineGenerator {
constructor(events) {
this.events = events.map(event => ({
...event,
date: 2024-05-12 10:00:00
})).sort((a, b) => a.date.valueOf() - b.date.valueOf());
}

// 按月份分组事件 getGroupedEvents() {
const groups = {};

this.events.forEach(event => {
const monthYear = event.date.format('YYYY-MM');

if (!groups[monthYear]) {
groups[monthYear] = {
title: event.date.format('YYYY 年 MM 月'),
events: []
};
}

groups[monthYear].events.push({
...event,
displayDate: event.date.format('MM 月 DD 日 HH:mm'),
isToday: event.date.isSame(moment(), 'day'),
isPast: event.date.isBefore(moment(), 'day'),
isFuture: event.date.isAfter(moment(), 'day')
});
});

return Object.entries(groups).map(([key, value]) => value);
}

// 获取最近的 N 个事件 getRecentEvents(n = 5) {
const now = moment();
return this.events
.filter(event => event.date.isSameOrBefore(now, 'minute'))
.sort((a, b) => b.date.valueOf() - a.date.valueOf())
.slice(0, n)
.map(event => ({
...event,
daysAgo: now.diff(event.date, 'day'),
hoursAgo: now.diff(event.date, 'hour')
}));
}

// 获取即将发生的 N 个事件 getUpcomingEvents(n = 5) {
const now = moment();
return this.events
.filter(event => event.date.isAfter(now, 'minute'))
.sort((a, b) => a.date.valueOf() - b.date.valueOf())
.slice(0, n)
.map(event => ({
...event,
daysFromNow: event.date.diff(now, 'day'),
hoursFromNow: event.date.diff(now, 'hour')
}));
}

// 计算事件密度(每段时间内的事件数量)
getEventDensity(interval = 'week') {
const density = {};
const intervals = new Set();

this.events.forEach(event => {
const intervalKey = event.date.startOf(interval).valueOf();
intervals.add(intervalKey);

if (!density[intervalKey]) {
density[intervalKey] = {
date: 2024-05-12 10:00:00
count: 0,
events: []
};
}

density[intervalKey].count++;
density[intervalKey].events.push(event);
});

return Array.from(intervals)
.map(key => density[key])
.sort((a, b) => a.date.valueOf() - b.date.valueOf());
}
}

// 使用示例 const events = [
{ id: 1, title: '项目启动', date: 2024-05-12 10:00:00
{ id: 2, title: '需求评审', date: 2024-05-12 10:00:00
{ id: 3, title: '开发完成', date: 2024-05-12 10:00:00
{ id: 4, title: '测试开始', date: 2024-05-12 10:00:00
{ id: 5, title: '发布上线', date: 2024-05-12 10:00:00
];

const timeline = new TimelineGenerator(events);
console.log(timeline.getGroupedEvents());
console.log(timeline.getRecentEvents());
console.log(timeline.getEventDensity('week'));

性能优化和最佳实践

1. 避免重复解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 不好的做法:重复解析相同的日期字符串 function inefficientParsing(dates) {
return dates.map(dateStr => {
const momentObj = moment(dateStr); // 每次都解析 return {
formatted: momentObj.format('YYYY-MM-DD'),
isFuture: momentObj.isAfter(moment())
};
});
}

// 更好的做法:使用缓存 class MomentCache {
constructor() {
this.cache = new Map();
}

get(dateStr, format) {
const key = `${dateStr}_${format || 'default'}`;

if (!this.cache.has(key)) {
const parsed = moment(dateStr, format);
this.cache.set(key, parsed);
}

return this.cache.get(key);
}

clear() {
this.cache.clear();
}
}

// 使用缓存 const cache = new MomentCache();
function efficientParsing(dates) {
const now = moment(); // 缓存当前时间 return dates.map(dateStr => {
const parsed = cache.get(dateStr);
return {
formatted: parsed.format('YYYY-MM-DD'),
isFuture: parsed.isAfter(now)
};
});
}

2. 批量操作优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 批量日期处理优化 class BatchDateProcessor {
constructor() {
this.now = moment(); // 预先计算当前时间
}

processDates(dates, operations) {
// 一次性解析所有日期 const parsedDates = dates.map(dateStr => moment(dateStr));

// 应用批量操作 return parsedDates.map(parsed => {
const result = { parsed };

operations.forEach(op => {
switch (op.type) {
case 'format':
result[op.field || 'formatted'] = parsed.format(op.format || 'YYYY-MM-DD');
break;
case 'compare':
result[op.field || 'isAfter'] = parsed.isAfter(this.now, op.unit || 'day');
break;
case 'calculate':
result[op.field || 'calculated'] = parsed[op.method](op.amount, op.unit).format('YYYY-MM-DD');
break;
case 'diff':
result[op.field || 'difference'] = parsed.diff(moment(op.reference), op.unit || 'days');
break;
}
});

return result;
});
}

// 批量比较日期范围 inDateRange(dates, start, end) {
const startDate = moment(start);
const endDate = moment(end);

return dates.map(dateStr => {
const date = moment(dateStr);
return date.isBetween(startDate, endDate, 'day', '[]');
});
}
}

3. 内存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 注意内存泄漏 class DateManager {
constructor() {
this.longRunningDates = [];
}

// 可能造成内存泄漏的做法 addDateWithCallback(dateStr) {
const date = moment(dateStr);

// 存储对 moment 对象的引用,可能导致内存泄漏 this.longRunningDates.push(date);

// 设置长期存在的定时器 const interval = setInterval(() => {
console.log(date.format('YYYY-MM-DD HH:mm:ss'));
}, 60000);

// 忘记清理定时器
// 这样会导致 date 对象无法被垃圾回收
}

// 更好的做法 addDateWithProperCleanup(dateStr) {
const date = moment(dateStr);
let intervalId;

const updateHandler = () => {
console.log(date.format('YYYY-MM-DD HH:mm:ss'));
};

intervalId = setInterval(updateHandler, 60000);

// 返回清理函数 return () => {
clearInterval(intervalId);
// 从数组中移除引用 const index = this.longRunningDates.indexOf(date);
if (index > -1) {
this.longRunningDates.splice(index, 1);
}
};
}

cleanup() {
// 清理所有资源 this.longRunningDates = [];
}
}

常见问题和解决方案

1. 时区问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 时区相关问题处理 class TimeZoneHelper {
static convertToLocalTime(utcTimeString, targetTimeZone = 'Asia/Shanghai') {
// 从 UTC 时间创建 moment 对象 const utcTime = moment.utc(utcTimeString);

// 转换到目标时区 const localTime = utcTime.clone().tz(targetTimeZone);

return localTime;
}

static convertToUTC(localTimeString, sourceTimeZone = 'Asia/Shanghai') {
// 从特定时区创建时间 const localTime = moment.tz(localTimeString, sourceTimeZone);

// 转换为 UTC
const utcTime = localTime.clone().utc();

return utcTime;
}

static getTimeDiffBetweenTimeZones(time, zone1, zone2) {
const timeInZone1 = moment.tz(time, zone1);
const timeInZone2 = moment.tz(time, zone2);

return timeInZone1.diff(timeInZone2, 'hours');
}

// 检测用户时区 static getUserTimeZone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}

// 安全的时区转换 static safeTimezoneConvert(date, fromZone, toZone) {
try {
const original = moment.tz(date, fromZone);
return original.clone().tz(toZone);
} catch (error) {
console.warn('时区转换失败,使用本地时间:', error);
return moment(date);
}
}
}

2. 日期边界问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 日期边界处理 class DateBoundaryHelper {
// 获取月份的天数 static getDaysInMonth(year, month) {
return moment([year, month]).daysInMonth();
}

// 闰年检查 static isLeapYear(year) {
return moment([year]).isLeapYear();
}

// 安全的月份操作(防止月份溢出)
static addMonthsSafely(date, months) {
const originalDate = moment(date);
const targetMonth = originalDate.month() + months;
const targetYear = originalDate.year() + Math.floor(targetMonth / 12);
const actualMonth = targetMonth % 12;

// 创建目标月份的第一天 const targetFirstDay = moment([targetYear, actualMonth, 1]);

// 获取目标月份的天数 const targetDaysInMonth = targetFirstDay.daysInMonth();

// 使用原始日期的日期部分,但不超过目标月份的最大日期 const targetDate = Math.min(originalDate.date(), targetDaysInMonth);

return moment([targetYear, actualMonth, targetDate,
originalDate.hour(), originalDate.minute(), originalDate.second()]);
}

// 日期边界检查 static validateDateRange(date, minDate, maxDate) {
const d = moment(date);
const min = moment(minDate);
const max = moment(maxDate);

if (d.isBefore(min, 'day')) {
throw new Error(`日期不能早于 ${min.format('YYYY-MM-DD')}`);
}

if (d.isAfter(max, 'day')) {
throw new Error(`日期不能晚于 ${max.format('YYYY-MM-DD')}`);
}

return true;
}
}

最佳实践建议

1. 日期常量定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 日期常量和工具 const DATE_UTILS = {
// 常用日期格式 FORMATS: {
DATE: 'YYYY-MM-DD',
DATETIME: 'YYYY-MM-DD HH:mm:ss',
TIME: 'HH:mm:ss',
TIMESTAMP: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]'
},

// 时间跨度常量(毫秒)
TIME_SPANS: {
SECOND: 1000,
MINUTE: 60 * 1000,
HOUR: 60 * 60 * 1000,
DAY: 24 * 60 * 60 * 1000,
WEEK: 7 * 24 * 60 * 60 * 1000
},

// 常用日期 get TODAY() {
return moment().startOf('day');
},
get YESTERDAY() {
return moment().subtract(1, 'day').startOf('day');
},
get TOMORROW() {
return moment().add(1, 'day').startOf('day');
}
};

2. 日期验证工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 日期验证工具 const DateValidation = {
isDateInRange(date, minDate, maxDate, unit = 'day') {
const d = moment(date);
const min = moment(minDate);
const max = moment(maxDate);

return d.isBetween(min, max, unit, '[]');
},

isBusinessHours(dateTime, startHour = 9, endHour = 18) {
const d = moment(dateTime);
const hour = d.hour();
return hour >= startHour && hour < endHour;
},

isWeekend(date) {
const d = moment(date);
return d.day() === 0 || d.day() === 6;
},

isValidBusinessDay(date) {
const d = moment(date);
return !this.isWeekend(d) && d.isValid();
}
};

迁移到现代替代方案

从 Moment.JS 迁移到 Day.JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Moment.JS 代码示例 const momentDate = moment('2024-01-01');
const formatted = momentDate.format('YYYY-MM-DD');
const isAfter = momentDate.isAfter(moment());

// Day.JS 等效代码 import dayjs from 'dayjs';
const dayjsDate = dayjs('2024-01-01');
const formatted = dayjsDate.format('YYYY-MM-DD');
const isAfter = dayjsDate.isAfter(dayjs());

// 迁移检查列表 const migrationGuide = {
// 基本替换
'moment()': 'dayjs()',
'moment(date)': 'dayjs(date)',
'.format()': '.format()',
'.isAfter()': '.isAfter()',
'.isBefore()': '.isBefore()',
'.add()': '.add()',
'.subtract()': '.subtract()',

// 需要额外导入的插件
'.startOf()/.endOf()': '需要 import relativeTime from \'dayjs/plugin/relativeTime\'',
'.fromNow()/.toNow()': '需要 import relativeTime from \'dayjs/plugin/relativeTime\'',
'.isBetween()': '需要 import isBetween from \'dayjs/plugin/isBetween\'',
'.daysInMonth()': 'dayjs 内置支持',

// 时区需要特殊处理
'moment-timezone': '需要 import timezone from \'dayjs/plugin/timezone\''
};

总结

  • Moment.JS 提供了丰富的时间处理功能,API 设计直观易用
  • 注意性能问题,避免在循环中重复创建 moment 对象
  • 在新项目中建议使用 dayjs、date-fns 等现代替代方案
  • 处理时区时要特别小心,确保用户看到正确的本地时间
  • 合理使用插件功能,避免加载不必要的功能
  • 在企业级应用中,建立统一的日期处理规范

Moment.JS 虽然不再是新项目的首选,但在维护现有项目时仍然非常重要。掌握其核心功能和最佳实践,能帮助我们更好地处理各种日期相关的业务需求。

扩展阅读

  • Moment.JS Official Documentation
  • Migrating from Moment.JS
  • Date and Time Libraries Comparison
  • Internationalization Best Practices
  • Javascript Date Performance

参考资料

  • Moment.JS GitHub Repository: https://github.com/moment/moment
  • Moment-Timezone: https://momentjs.com/timezone/
  • Day.JS Documentation: https://day.JS.org/
  • Date-fns Library: https://date-fns.org/
bulb