先记住这个答案
Intl.RelativeTimeFormat 的 format(value, unit) 把一个数值和单位格式化成相对时间。value 的正负决定方向:format(-5, 'minute') 是「5分钟前」,format(5, 'minute') 是「5分钟后」。构造时传 numeric: 'auto',遇到 -1 天、1 天、0 天这种有专门说法的情况,会输出「昨天」「明天」「今天」。它只负责措辞,不接收两个 Date。算时间差、决定用秒还是用天、怎么取整,都是调用方的事。
- 负数表示过去,正数表示未来
numeric: 'auto'会输出昨天、明天、上周- 它不接收两个 Date,时间差和单位要自己算
- 按 24 小时算「昨天」,跨午夜和夏令时会不准
numeric 的两种模式
同一组数值,默认模式和 auto 模式的输出对比一下就清楚了。
always 和 auto 对比JavaScriptconst always = new Intl.RelativeTimeFormat('zh-CN') // 默认 numeric: 'always'
const auto = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' })
const cases = [[-5, 'minute'], [-1, 'day'], [0, 'day'], [1, 'day'], [-1, 'week']]
for (const [value, unit] of cases) {
console.log(value, unit, always.format(value, unit), auto.format(value, unit))
}查看输出与解释
-5 minute 5分钟前 5分钟前
-1 day 1天前 昨天
0 day 0天后 今天
1 day 1天后 明天
-1 week 1周前 上周默认模式总是用数字,0 天会说成「0天后」,很别扭。auto 模式在语言有专门词汇时换成自然说法,其他情况和默认模式一样。评论、消息列表这类场景基本都用 auto。
单位写单数 minute 或复数 minutes 都可以,规范两种都接受。但写成 min 这种缩写会抛 RangeError。
自己写一个自动选单位的 timeAgo
实际业务拿到的是一个时间戳,要自己算出差值,再按大小选合适的单位。下面是一个最简版本,now 作为参数传入,方便测试。
const rtf = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' })
const UNITS = [['year', 31536000], ['month', 2592000], ['day', 86400], ['hour', 3600], ['minute', 60]]
function timeAgo(target, now) {
const seconds = Math.round((target - now) / 1000) // 过去为负,未来为正
for (const [unit, size] of UNITS) {
if (Math.abs(seconds) >= size) return rtf.format(Math.trunc(seconds / size), unit)
}
return rtf.format(seconds, 'second')
}
const now = new Date('2026-09-10T12:00:00Z')
console.log(timeAgo(new Date('2026-09-10T11:55:00Z'), now))
console.log(timeAgo(new Date('2026-09-09T12:00:00Z'), now))
console.log(timeAgo(new Date('2026-06-10T12:00:00Z'), now))
console.log(timeAgo(new Date('2026-09-10T12:00:30Z'), now))查看输出与解释
5分钟前
昨天
3个月前
30秒钟后思路是从大单位往小单位找,第一个「差值大于等于这个单位长度」的就用它,然后除以单位长度取整。target - now 让过去的时间自然得到负数,直接交给 format(),不用自己拼「前」「后」。
月按 30 天、年按 365 天算是近似值,对评论区够用了。Math.trunc 是向零取整,89 秒会显示成「1分钟前」;想要四舍五入就换成 Math.round,但要注意 59 分 40 秒会被说成「60分钟前」,最好再处理一下进位。
- 「昨天」按 24 小时算并不准
- 上面的写法里,「昨天」其实是「24 到 48 小时之前」。晚上 11 点看早上 1 点发的评论,差了 22 小时,显示「22小时前」,但按日历它是今天。真正的「昨天」是日历概念,要按用户时区比较两个时间的日期,还要考虑夏令时那天只有 23 或 25 小时。对准确性有要求的场景,比如聊天记录的日期分组,用日期差而不是毫秒差。
容易答错的地方
- 把过去的时间传成正数
- 正数是未来。算差值时写成
now - target,过去的时间就变成了正数,页面上显示「5分钟后」。统一用target - now,过去为负。 - 以为它能直接接收两个 Date
format()只接收数值和单位,传 Date 进去不会帮你算差值。选单位、取整、阈值这些都是产品规则,每个产品不一样,所以 API 故意没有包办。
面试官还会怎么问?
「刚刚」这种说法怎么实现?
Intl.RelativeTimeFormat 在 auto 模式下 format(0, 'second') 会输出「现在」。产品想要「刚刚」,可以在差值小于某个阈值时直接返回自己的文案,这属于产品措辞,不走格式化器。
formatToParts() 用在什么地方?
需要把数字单独加粗或换颜色时用。英文 formatToParts(-5, 'minute') 会拆成数字 5 和文本「 minutes ago」两段,按顺序渲染就不会破坏语序。
列表里几百条评论都要显示相对时间,怎么做?
formatter 建一个全局复用,不要每条都 new。定时刷新也只用一个定时器,比如每分钟统一重新计算一次,而不是每条评论挂一个 setInterval。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。