日期与国际化:时间点、日历和显示格式
“明天”“24 小时后”“2026 年 9 月 14 日”描述的不是同一种东西。Date 擅长保存时间点;本地年月日是时间点在某个时区中的解释;Intl 负责按地区展示。把三层分开,日期代码才不会在换个城市后突然变脸。
Date 保存毫秒时间戳,不保存时区名称
const instant = new Date('2026-09-14T00:00:00Z');
console.log(instant.getTime()); // 1789344000000
console.log(instant.toISOString()); // 2026-09-14T00:00:00.000Z
console.log(new Date(0).toISOString()); // 1970-01-01T00:00:00.000ZgetTime 返回相对 Unix 纪元的毫秒数,Date.now 直接取得当前时间戳。时间点本身不携带“上海”或“纽约”的地理标识,toISOString 用 UTC 显示,getHours 等本地方法按宿主时区解释。
new Date(year, monthIndex, day, ...) 使用本地时区,月份从 0 开始;Date.UTC(...) 使用 UTC 并返回时间戳。getDate 是月内日期,getDay 是星期,星期日为 0。getFullYear/getMonth/getHours 对应本地组件,加入 UTC 的版本如 getUTCFullYear 读取 UTC 组件。
解析必须说明偏移
console.log(new Date('2026-09-14').toISOString()); // 2026-09-14T00:00:00.000Z
console.log(new Date('2026-09-14T08:00:00+08:00').toISOString()); // 同一 UTC 时间点
console.log(Number.isNaN(new Date('not-a-date').getTime())); // true标准日期字符串只含日期时按 UTC 解释;有时间但没有偏移时按本地时区解释,这种历史差异特别容易出错。跨系统交换明确用带 Z 或偏移的格式。非标准字符串如 '09/14/26' 的解析不能作为稳定契约。
无效 Date 的时间戳为 NaN,toISOString 会抛 RangeError。不能仅凭 value instanceof Date 断定其有效。组件构造还会自动进位,例如 UTC 的第 13 个月进入下一年;这对日期运算方便,但不等于输入校验。
“加一天”与“加 24 小时”
const date = new Date('2026-01-31T00:00:00Z');
date.setUTCDate(date.getUTCDate() + 1);
console.log(date.toISOString()); // 2026-02-01T00:00:00.000ZsetDate/setMonth/setFullYear 等修改原 Date 并返回新时间戳;要保留输入先 new Date(original.getTime())。在实行夏令时的本地时区,下一天同一墙上时间不一定相隔 24 小时。固定时长加 24 * 60 * 60 * 1000,日历日期推进则按明确日历和时区规则处理。
getTimezoneOffset 是 UTC 减本地时间的分钟数,东八区通常为 -480,同一地区的历史或夏令时可能改变偏移。生日这样的“纯日期”不一定需要一个 UTC 午夜时间点,可以先保存明确的年月日语义。
月份加一,不代表自动停在下个月最后一天
const direct = new Date('2026-01-31T00:00:00Z');
direct.setUTCMonth(direct.getUTCMonth() + 1);
console.log(direct.toISOString()); // 2026-03-03T00:00:00.000Z保留原日期31,再把月份设为二月,相当于要求“二月31日”。Date会将超出二月28天的部分继续推进,所以落到三月3日。如果需求是“下个月同日,若不存在则用月末”,需要明确实现截断策略:
const source = new Date('2026-01-31T00:00:00Z');
const target = new Date(source.getTime());
const day = target.getUTCDate();
target.setUTCDate(1);
target.setUTCMonth(target.getUTCMonth() + 1);
const lastDay = new Date(Date.UTC(
target.getUTCFullYear(), target.getUTCMonth() + 1, 0,
)).getUTCDate();
target.setUTCDate(Math.min(day, lastDay));
console.log(target.toISOString()); // 2026-02-28T00:00:00.000Z
console.log(source.toISOString()); // 2026-01-31T00:00:00.000Z先锚定1日,避免加月时由31自动溢出;下一月的第0日表示目标月最后一天;最后在原日期与月末中取较小值。这里用2026年的UTC日期演示政策,不把它包装成已经处理所有年份、日历和时区的通用日期库。
日期校验也受自动进位影响。用户输入“2月31日”可能得到一个有效Date,但得到的年月日已不是输入的年月日;需要逐项回读对比,而不是只检查时间戳非NaN。自动校准适合运算,不负责拒绝不合法的日历输入。
Intl 指定地区和选项
const date = new Date('2026-09-14T00:00:00Z');
const formatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false,
});
console.log(formatter.format(date)); // 表达上海时间 2026-09-14 08:00,标点依实现
console.log(formatter.formatToParts(date).find(part => part.type === 'hour').value); // 08地区决定语言与格式,timeZone 决定同一时间点在哪个时区解释。它们不是同一个选项。不要把本地化输出当作机器可解析的固定字符串;不同 ICU/CLDR 数据版本可能有标点差异。需要稳定结构时使用 formatToParts,按 type 取值。
同一格式反复使用时复用 formatter。resolvedOptions 查看实际采用的配置,supportedLocalesOf 检查地区支持;formatRange/formatRangeToParts 适合展示区间,目标运行时仍需核对。
用同一对时间点看见夏令时的影响
以下例子只使用带偏移的明确时间点,并由Intl指定纽约时区;不依赖你电脑当前在哪个时区:
const before = new Date('2026-03-07T12:00:00-05:00');
const nextNoon = new Date('2026-03-08T12:00:00-04:00');
const after24Hours = new Date(before.getTime() + 24 * 60 * 60 * 1000);
const view = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York', hour: 'numeric', hour12: false,
});
console.log((nextNoon - before) / 3600000); // 23
console.log(view.format(nextNoon), view.format(after24Hours)); // 12 13两个日期的“本地中午”相隔23小时,因为时区在期间向前调整了一小时;固定加24小时则落到当地13点。时间差计算没有错,错的是把日历日与固定时长当成同一单位。
Intl在这里负责显示,不负责推断“下一天同一时刻”的业务意图。需要安排重复的本地会议,应保存地区时区与当地时间规则,而不是只保存一个固定UTC偏移;-05:00描述某时刻的偏移,America/New_York才提供该地区随日期变化的规则。
Intl 不只有日期格式化
const money = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' });
console.log(money.format(12.5)); // 人民币金额的本地化文本
const relative = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' });
console.log(relative.format(-1, 'day')); // 昨天
const collator = new Intl.Collator('en', { numeric: true });
console.log(['chapter10', 'chapter2'].toSorted(collator.compare)); // ['chapter2', 'chapter10']NumberFormat 处理货币、百分比、单位和数字精度显示,但不执行汇率换算。RelativeTimeFormat 把已经算好的数量显示成“昨天”等文本,不替你计算时间差。Collator 处理语言敏感比较,不保证返回值恰好是 -1 或 1。
PluralRules 给出语言对应的复数类别;ListFormat 连接自然语言列表;DisplayNames 显示地区或语言名称;Segmenter 分字素、词、句;Locale 表示和分析地区标识。Intl.getCanonicalLocales 规范化地区名称,supportedValuesOf 查询部分受支持值。较新的 DurationFormat 格式化时长,采用前单独检查,不假定 Node 22 的所有发行版支持。
Temporal 是更细致地区分时间点、纯日期和带时区日期时间的 API 方向;其当前支持不作为本书 Date 示例的隐含前提。需要采用时按目标运行时核验,不把提案状态和浏览器落地混成一个结论。
测时不要依赖可调整的墙上时钟
Date.now 适合记录发生时刻,系统时钟调整会影响它。测一段代码经过多久通常使用宿主提供的单调计时 performance.now。浏览器和 Node 都有相应实现,测量还会受预热、调度与噪声影响,单次结果不是性能定理。
练习
'2026-09-14'与'2026-09-14T00:00:00'在非 UTC 时区为什么可能不是同一个时间点?不修改输入 Date,产生 24 小时后的新 Date,并解释它与本地“明天同一时刻”的区别。
为何1月31日直接setUTCMonth到二月可能落到三月?如果规则是下月同日且超出时取月末,应按什么顺序操作?