前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

moment时间处理常用方法与现代替代方案

首页2018-12-06 11:08:24Front-End
JavaScriptmoment日期处理

后台管理系统的筛选栏最容易碰到这类需求。产品要「今天、昨天、近 7 天、近 30 天、本月」五个快捷按钮,每个按钮点下去要给后端一对 YYYY-MM-DD 的起止日期。用原生 Date 手写,光是「上个月的今天」就得先判断上个月有多少天,代码写到第三个按钮就开始打结。moment 把这类事情压成了一行。

这篇是我当年用 moment 时攒的一份速查笔记,这次回头重新整理,顺手把里面几处写错的地方改了,也补上 2018 年之后发生的变化。moment 官方现在已经把项目置于维护模式,新项目该怎么选,最后一节会说。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • moment 对象是可变的,这个特性带来的坑
  • format 占位符里 MM 和 mm、HH 和 hh 的区别
  • 取年月日、取周几,以及 day 和 weekday 到底差在哪
  • add / subtract 做时间加减
  • startOf / endOf 拿一天、一月的起止时刻
  • fromNow 和 diff 算相对时间与时间差
  • 把这些拼成筛选栏的日期常量表
  • moment 进入维护模式之后的替代方案与迁移思路

# 一、先记住一件事,moment 对象是可变的

这是 moment 最容易翻车的地方,也是我建议放在最前面讲的原因。

const base = moment('2018-12-06');
const next = base.add(1, 'days');

console.log(base.format('YYYY-MM-DD')); // 2018-12-07
console.log(next.format('YYYY-MM-DD')); // 2018-12-07
@前端进阶之旅: 代码已经复制到剪贴板

add 不返回新对象,它直接改掉了 base 自己,然后把 base 返回给你。所以 base 和 next 是同一个对象。你要是把一个 moment 对象存进 state 或者当成常量复用,某个地方不小心 add 了一下,其他所有地方跟着一起偏。

这个坑我踩过,排查了半天才反应过来是同一个引用。

绕开的办法只有一个,每次都从 clone() 出发:

const base = moment('2018-12-06');
const next = base.clone().add(1, 'days');

console.log(base.format('YYYY-MM-DD')); // 2018-12-06
console.log(next.format('YYYY-MM-DD')); // 2018-12-07
@前端进阶之旅: 代码已经复制到剪贴板

或者干脆每次都重新 moment() 一个新的。下面所有示例都写成 moment().xxx() 这种一次性的链式调用,就是因为一次性对象没有共享,也就没有这个问题。

# 二、format 的占位符,MM 和 mm 是两回事

最基础的用法是取当前时间,按中文格式输出。

var now = moment().format("YYYY年MM月DD日");
@前端进阶之旅: 代码已经复制到剪贴板

任意时间戳格式化,注意 moment 接收的是毫秒级时间戳:

var t1 = moment(1411641720000).format('YYYY-MM-DD HH:mm:ss');
@前端进阶之旅: 代码已经复制到剪贴板

如果你手上是后端给的秒级时间戳(10 位),得先乘 1000:

// 格式化秒级时间戳为 2018-12-06 12:21:00
moment(unixSeconds * 1000).format('YYYY-MM-DD HH:mm:ss')
@前端进阶之旅: 代码已经复制到剪贴板

这里有个坑要注意,占位符是区分大小写的,而且区分得很较真:

占位符 含义 常见误用
YYYY 四位年 yyyy 在 moment 里不是年
MM 两位月份 被当成分钟
DD 两位日期 dd 是星期几的缩写
HH 24 小时制小时 hh 是 12 小时制
mm 两位分钟 被写成 MM
ss 两位秒 被写成 SS,S 是毫秒的小数位

我那份老笔记里就有这么一行:

// 这行是错的
now = moment().format('YYYY-MM-DD:HH:MM:SS');
@前端进阶之旅: 代码已经复制到剪贴板

HH:MM:SS 里的 MM 输出的是月份,SS 输出的是毫秒小数位,所以拿到的时间部分完全不对。正确写法是:

now = moment().format('YYYY-MM-DD HH:mm:ss');
@前端进阶之旅: 代码已经复制到剪贴板

这个错误特别隐蔽,因为月份和分钟在某些时刻碰巧看起来都像合法数字,测试的时候未必一眼看得出来。

除了自己拼占位符,moment 还提供了几个本地化的短名:

now = moment().format('L');   // 10/22/2016,随 locale 变化
now = moment().format('LL');  // October 22, 2016,随 locale 变化
@前端进阶之旅: 代码已经复制到剪贴板

L 和 LL 输出什么完全取决于当前加载的 locale。默认是 en,所以是美式日期。要中文得先引入并切换到 zh-cn。这类跟 locale 绑定的格式,我一般只在给用户看的展示层用,传给后端的参数永远写死成 YYYY-MM-DD,免得换个环境就变样。

# 三、取年月日和周几

单独取某个部分,moment 提供了一组同名方法。

var t14 = moment().year();
var t15 = moment().month(); // 此处月份从 0 开始,当前月要 +1
var t16 = moment().date();
@前端进阶之旅: 代码已经复制到剪贴板

month() 从 0 开始这件事跟原生 Date.prototype.getMonth() 是一致的,属于历史包袱。要拿到人看的月份记得加一,或者干脆用 format('MM'),那个是从 1 开始的。

date() 是「几号」,别和 day() 搞混,day() 是「星期几」。

顺着这个说到 day 和 weekday 的差别,这是我老笔记里另一处错误:

// 老笔记写的是「获取前一天日期」,其实不是
var t11 = moment().day(0).format('YYYY-MM-DD');
@前端进阶之旅: 代码已经复制到剪贴板

day(0) 拿到的是本周的周日,不是前一天。moment 的 day() 固定以周日为 0、周六为 6,跟 locale 无关。要拿前一天,用后面讲的 subtract 才对。

var t12 = moment().weekday(5).format('YYYY-MM-DD');
var t13 = moment().weekday(-3).format('YYYY-MM-DD');
@前端进阶之旅: 代码已经复制到剪贴板

weekday() 和 day() 的区别在于,它是相对「本地化的一周第一天」来数的。在默认的 en locale 下一周从周日开始,weekday(5) 是周五;切到 zh-cn,一周从周一开始,同样的 weekday(5) 就变成周六了。负数会往前一周数,weekday(-3) 落在上一周,具体是周几同样看 locale。

所以老笔记里「获取本周五」「获取上周五」这两条注释,只在某个特定 locale 下成立。

你要是在做跨语言的项目,我的建议是别用 weekday,用 day() 加显式的加减,行为是确定的,不会因为某个地方 moment.locale('zh-cn') 一句就全变了。

# 四、加减时间用 add 和 subtract

这两个方法是 moment 用得最多的部分,签名都是 (数量, 单位)。

// 获取上个月今天的日期
var t18 = moment().subtract(1, 'months').format('YYYY-MM-DD');

// 获取上个月,只要年月
var t19 = moment().subtract(1, 'months').format('YYYY-MM');

// 获取前一天
var t20 = moment().subtract(1, 'days').format('YYYY-MM-DD');

// 获取两个小时之后
var t22 = moment().add(2, 'hours').format('YYYY-MM-DD HH:mm:ss');

// 获取五天前,例如今天 2018-7-23,拿到 2018-7-18
var t23 = moment().subtract(5, 'days').format('YYYY-MM-DD');
@前端进阶之旅: 代码已经复制到剪贴板

单位可以写单数、复数或者缩写,'day'、'days'、'd' 是同一个意思。add 传负数等价于 subtract,所以下面两行结果一样:

let beforeDay = moment().add(-5, 'day').toDate();
beforeDay = moment().subtract(5, 'day').toDate();
@前端进阶之旅: 代码已经复制到剪贴板

按月加减这块有个行为值得知道。moment 做月份加减时会保护「日」不溢出,比如 3 月 31 号减一个月,2 月没有 31 号,结果是 2 月 28 号(闰年 29 号),而不是滚到 3 月 3 号去。这跟你手写 setMonth 的默认行为是不一样的,setMonth 会溢出。

回到实际场景,这个差别决定了「上个月今天」这种需求能不能一行搞定。

toDate() 会把 moment 对象转回原生 Date,需要把值交给不认识 moment 的第三方库时会用到:

var now = moment().toDate();
@前端进阶之旅: 代码已经复制到剪贴板

# 五、startOf 和 endOf 拿区间端点

筛选栏要的是「这一天从 00:00:00 到 23:59:59」这种闭区间,手写会很啰嗦。

// 这个月月初
let startMonth = moment().startOf('month').toDate();

// 今天开始的时间
let dayOfStart = moment().startOf('day').toDate();

// 今天结束的时间
let dayOfEnd = moment().endOf('day').toDate();
@前端进阶之旅: 代码已经复制到剪贴板

startOf('day') 把时分秒毫秒全部清零,endOf('day') 设成 23:59:59.999。单位除了 day、month,还有 year、week、hour 这些。

注意 endOf 给的是 .999 毫秒而不是下一天的 00:00:00。如果后端那边是按左闭右开区间查的,直接把 endOf('day') 传过去会漏掉那一毫秒里的数据。这种边界差一点点的问题,量小的时候根本发现不了。稳妥的做法是和后端约定清楚区间是闭是开,然后统一用 startOf('day') 加上「下一天的 startOf('day')」这种左闭右开的写法。

startOf('week') 同样受 locale 影响,一周从周日还是周一开始,取决于当前 locale。

# 六、fromNow 和 diff

fromNow() 给的是「三天前」「两个月后」这种相对描述,列表页展示时间时很常用:

// 获取到现在的年限,如果不满一年显示出具体几个月
let years = moment('2020-12-31').fromNow();
@前端进阶之旅: 代码已经复制到剪贴板

输出的语言同样跟着 locale 走,默认英文的 in 2 years,切到 zh-cn 才是「2 年后」。

diff 算的是两个时间的差值,第二个参数是单位:

fe
  • 一、先记住一件事,moment 对象是可变的
  • 二、format 的占位符,MM 和 mm 是两回事
  • 三、取年月日和周几
  • 四、加减时间用 add 和 subtract
  • 五、startOf 和 endOf 拿区间端点
  • 六、fromNow 和 diff
  • 七、把这些拼成筛选栏的常量表
  • 八、和 moment 无关,但常一起写的时间列表
  • 九、2018 之后,moment 变成了「不推荐用于新项目」
  • 十、现在该选什么
  • 总结
  • 参考

← oppo快应用开通账号服务获取openid记录lodash常用API →