Day.js — 用 Moment 风格 API 包装原生 Date 的不可变日期库
已复核Day.js 是一个包装原生 Date 的 JavaScript 日期库。日常类比:像给原生 Date 套一层会说话的外壳——方法名尽量跟 Moment 一样,但每次改时间都复印一张新卡,而不是在原卡上涂改。
import dayjs from "dayjs";dayjs("2026-05-30").add(1, "month").subtract(7, "day").format("YYYY-MM-DD");dayjs(...) 返回 Dayjs 实例。核心只提供 parse、加减、比较、format 和 locale;时区、自定义解析、相对时间等要 dayjs.extend(plugin) 才挂到原型上。
不理解固定 1.11.23 的包装合同,下面这些事会对不上:
- 为什么
d.add(1, "day")单独写一行看起来“没反应” - 为什么
dayjs("12-25-1995", "MM-DD-YYYY")在没装customParseFormat时并不按第二个参数解析 - 为什么
.tz()文档总是先extend(utc)再extend(timezone) - 为什么同样叫 immutable,它和 date-fns 的形态完全不同:一边是 wrapper + 链式方法,一边是纯函数 + 原生 Date
固定版本的执行链可以拆成五步:
- 构造 wrapper:
dayjs(date)若已是 Day.js 对象就clone();否则把date放进 config,new Dayjs(cfg)。识别靠instanceof或$isDayjsObject。 - 解析成内部
Date:null→Invalid Date;undefined→ 现在;已有Date→ 再拷一份。不以Z结尾的字符串先走REGEX_PARSE,按本地字段组new Date(y, m, d, ...);其余交给new Date(date)。 - 缓存日历字段:
init()把$y/$M/$D/$H/...从内部$d读出。装了 utc plugin 且$u === true时改读 UTC getter。 - 公开变更先 clone:
set/add/subtract/locale都先复制再改。内部$set会改当前实例,但默认路径不把它暴露给调用方。 - plugin 是进程级副作用:
dayjs.extend(plugin)用plugin.$i保证同一函数只安装一次,随后改Dayjs.prototype或dayjs本身,对已有和未来实例都生效。
仓库 package.json 的 size-limit 把构建产物 dayjs.min.js 限制在 2.99 KB;这是仓库门槛,不是本页测过的 gzip 体积。
案例 1:链式加减必须接住返回值
Section titled “案例 1:链式加减必须接住返回值”const d = dayjs("2026-05-30");const later = d.add(1, "month").subtract(7, "day");d.format("YYYY-MM-DD"); // 2026-05-30later.format("YYYY-MM-DD"); // 2026-06-23add 对月/年走 set,对日/周按日历日推进,对时分秒按毫秒步进,最后都 Utils.w(...) 包成新实例。原对象的 $d 不变。
案例 2:自定义格式解析是 plugin,不是 core
Section titled “案例 2:自定义格式解析是 plugin,不是 core”import customParseFormat from "dayjs/plugin/customParseFormat";dayjs.extend(customParseFormat);dayjs("12-25-1995", "MM-DD-YYYY").format("YYYY-MM-DD");core 里第二个参数只有是 object 才会当 config;字符串 format 被丢掉,输入改走默认 REGEX_PARSE / new Date。装上 plugin 后,args[1] 才是 token 串,还可传 locale 与 strict。
案例 3:时区依赖 utc,并委派 Intl
Section titled “案例 3:时区依赖 utc,并委派 Intl”import utc from "dayjs/plugin/utc";import timezone from "dayjs/plugin/timezone";dayjs.extend(utc);dayjs.extend(timezone);
dayjs.tz("2026-05-30 10:00", "Asia/Shanghai");dayjs.tz.guess(); // Intl.DateTimeFormat().resolvedOptions().timeZonetimezone 用 Intl.DateTimeFormat#formatToParts 算偏移,内部调用 dayjs.utc。先 extend utc,否则 d.utc 不存在。库本身不带 IANA 数据包,行为随运行时 ICU 变化。
- 丢掉 chain 返回值:
d.add(1, "day")不改d。这是相对 Moment mutable API 最常见的迁移错误。 - 以为第二参数永远是 format:没装
customParseFormat时它只是被忽略的非 object。 - timezone 不先装 utc:plugin 源码用
d.utc(...)算墙钟偏移,顺序反了会在运行时炸掉。 - plugin / locale /
tz.setDefault都是全局的:一处extend影响整个进程;parseLocale默认还会改模块级当前 locale。 - format token 是 Moment 风格
YYYY-MM-DD:和 date-fns 的 Unicodeyyyy-MM-dd不是同一套。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 已有 Moment 链式写法,想尽量少改方法名
- 只要加减、格式化、比较,少量 plugin 就够
- 能接受时区数据来自运行时
Intl,而不是库内打包
不适用:
- 只要按函数 tree-shake,不想要 wrapper 或全局
extend - 需要把时区实例当
Date子类传给其他库 → 看 date-fns 的TZDate/in上下文 - 还没装 utc 就想用 IANA 时区
- 要把静态阅读升级成已测 bundle 或跨引擎时区结论
固定版本边界
Section titled “固定版本边界”- 本文绑定
iamkun/dayjs@4549b8d03307891143e8a50d39fcdab1f16f77cf。GitHub tagv1.11.23与 npmdayjs@1.11.23的gitHead指向同一提交。 - 固定源码含 37 个
src/plugin目录与 143 个src/locale文件;core 默认 locale 是en。 - 未安装依赖、未跑上游 Jest / Sauce、未测
dayjs.min.js体积,状态保持UNVERIFIED。
- API 兼容可以只换实现:公开方法跟 Moment 像,底层却是 clone-then-mutate。
- 极小 core 把能力推到 plugin:解析格式和时区都不是默认合同。
- 全局 extend 有安装顺序:utc 是 timezone 的前置函数,不是文档口味。
- 委派
Intl换体积:时区正确性跟着 Node/浏览器 ICU,不跟着这一个 npm 包。
const d = dayjs("2026-05-30"); d.add(1, "day");之后d的日期变了吗?- 未
extend(customParseFormat)时,dayjs("12-25-1995", "MM-DD-YYYY")会按MM-DD-YYYY解析吗? - 只
extend(timezone)、不extend(utc),dayjs.tz(...)在固定源码里能否完成偏移计算?
检查点:
- 不会。
add返回新实例。 - 不会。core 把非 object 的第二参数丢掉。
- 不能指望。
timezone调用d.utc。
- 文档:day.js.org
- 固定源码:iamkun/dayjs —— 本文绑定
4549b8d03307891143e8a50d39fcdab1f16f77cf - date-fns —— 同主题的纯函数路线,format token 与时区扩展都不同
- luxon —— Moment 团队的另一条 immutable class 路线
- temporal-polyfill —— 未来标准,不经过 Day.js wrapper
- date-fns —— 函数 + 原生 Date,对照 wrapper + chain
- luxon —— 时区/locale 同样借
Intl,但默认就是 class API - temporal-polyfill —— 标准日期模型,不再模拟 Moment
- js-joda —— 强类型 civil time,不包装可变
Date
- date-fns —— date-fns — 不造新类型,给原生 Date 配 200+ 个独立函数
- js-joda —— js-joda — 把 Java 的 java.time 整套搬进 JS
- luxon —— Luxon — 如果今天重写 Moment 应该长什么样
- react-intl —— react-intl — 让 React 应用按 ICU 标准说人话
- temporal-polyfill —— temporal-polyfill — 给 JavaScript 装上现代日期时间标准的备胎