JS getMonth() 为什么返回 0-11?获取当前月份的正确写法
深入解析 JavaScript 中 Date.prototype.getMonth() 返回 0-11 的历史历史原因与设计背景,并提供获取当前自然月份的多种正确写法、补零技巧、国际化方案及常见避坑指南。
new Date().getMonth() 时,返回的数值范围是 0 到 11,而不是日常认知中的 1 到 12。这是因为 1995 年 JavaScript 诞生时直接移植了 Java 1.0 的 Date 实现,而 Java 又沿袭了 C 语言标准库中将月份作为数组索引下标的底层设计。要获取日常业务中对应的实际月份,最直接、可靠的核心写法是给返回值加 1(即 new Date().getMonth() + 1)。本文将系统梳理这一机制的历史原委、不同业务场景下的标准取值与补零写法,以及在实际开发中必须严防的计算陷阱。
一、为什么 getMonth() 会返回 0 到 11?
许多初学 JavaScript 的开发者在第一次使用日期对象时,几乎都会被这个现象困扰:如果当前是 5 月,调用 getMonth() 得到的却是 4;如果要获取 1 月,得到的结果竟然是 0。更有趣的是,用于获取日期的 getDate() 返回的却是正常的 1 到 31。这种不一致的非对称设计究竟是出于怎样的考虑?
1. 寻根溯源:从 C 语言到 Java,再到 JavaScript
要理解这个设计,我们需要把时间拨回到 1970 年代。在 C 语言的运行时标准库头文件 <time.h> 中,定义了一个用于表示日历分解时间的结构体 struct tm。在这个结构体中,各字段的定义如下:
tm_mday:表示一个月中的第几天,取值范围为 1 ~ 31。tm_mon:表示自一月以来的月份数,取值范围为 0 ~ 11。
在早期的 C 语言编程实践中,开发者经常需要将数字月份转换为易于人类阅读的英文缩写(如 "Jan"、"Feb"、"Mar")。若将月份设计为以 0 开始的连续整数,开发者就可以直接使用月份数值作为字符串常量数组的索引下标进行常数级寻址:
const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
// 0 直接对应数组首项 months[0] -> "Jan",无需进行额外减 1 运算
const char *current_month_str = months[time_info->tm_mon];
1995 年初,Sun 公司发布了 Java 1.0,其标准库中的 java.util.Date 几乎全盘照搬了 C 语言 struct tm 的设计,保留了 getMonth() 返回 0-11 的约定。
同年 5 月,Netscape 工程师 Brendan Eich 受命在 10 天内为浏览器设计出一门脚本语言(最初名为 Mocha,后更名为 JavaScript)。管理层要求该语言的语法“看上去要像 Java”。在极其紧张的工期下,Brendan Eich 只能直接参照并复刻了 Java 1.0 的 java.util.Date API。由此,0 索引的 getMonth() 正式沉淀在 JavaScript 语言规范中。
2. 为什么 ECMAScript 规范至今不修改它?
Java 在推出 1.1 版本时,就已经意识到 java.util.Date 存在诸多设计缺陷,随即将其标记为废弃(Deprecated),并引导开发者使用 Calendar 以及后来的 java.time 包。然而,JavaScript 所处的 Web 运行环境具有特殊性。
Web 平台最核心的技术信条之一是“不破坏万维网”(Don't Break the Web)。全球数以千万计的网站和企业系统中,运行着大量写有 date.getMonth() + 1 的历史代码。如果 ECMAScript 标准委员会在某一版规范中将 getMonth() 更改为返回 1 到 12,所有现存未加修改的网站在计算月份时都会变成 2 到 13,引发无法估量的全球生产事故。因此,这个历史包袱被永久保留了下来。
二、获取当前月份的正确写法
针对不同的业务场景(如纯数字计算、双位字符串补零、多语言展示等),开发者应当选用与之相符的最佳实践。
1. 基础数值获取:偏移修正 + 1
当只需要获取当前月份的纯数字(1 ~ 12)用于数学判断、季度归类或周期计算时,最直接规范的写法就是在调用 getMonth() 后进行算术加 1:
// 获取当前时间的本地月份 (1 ~ 12)
const now = new Date();
const currentMonth = now.getMonth() + 1;
console.log(currentMonth); // 示例输出:5 (表示当前为 5 月)
console.log(typeof currentMonth); // "number"
2. 格式化为双位数字符串(补齐前导零)
在生成符合 ISO 8601 标准的日期格式(如 YYYY-MM-DD)或调用后端接口时,通常要求月份固定保持两位数字(例如 1 月表示为 "01",10 月表示为 "10")。推荐以下两种写法:
写法 A:使用现代标准的 padStart()(推荐)
ES2017 引入了 String.prototype.padStart(),能够以清晰直观的方式在字符串前填充指定字符至目标长度:
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, '0');
console.log(month); // 若为 5 月,输出示例:"05";若为 11 月,输出:"11"
// 完整组合标准年月:
const year = now.getFullYear();
const yearMonth = `${year}-${month}`; // "2026-05"
写法 B:基于 slice(-2) 的传统兼容写法
如果需要在非常老旧的运行环境或未配置 Babel 的历史项目中运行,可以使用传统的字符串切片方案:
const now = new Date();
// 在月份前强制拼接 '0',然后截取末尾两位
const month = ('0' + (now.getMonth() + 1)).slice(-2);
console.log(month); // 示例输出:"05" 或 "12"
3. 现代本地化与国际化方案:Intl.DateTimeFormat
如果业务需求是将月份展示给不同语言区域的用户(例如显示中文“五月”或英文缩写“May”),无需自行编写 switch-case 或月份映射数组,内置的 ECMA-402 Intl 国际化 API 提供了原生支持:
const now = new Date();
// 1. 获取两位数补零月份
const twoDigitMonth = new Intl.DateTimeFormat('zh-CN', { month: '2-digit' }).format(now);
console.log(twoDigitMonth); // 示例输出:"05"
// 2. 获取中文长文本月份名称
const fullMonthZh = new Intl.DateTimeFormat('zh-CN', { month: 'long' }).format(now);
console.log(fullMonthZh); // 示例输出:"五月"
// 3. 获取英文缩写月份名称
const shortMonthEn = new Intl.DateTimeFormat('en-US', { month: 'short' }).format(now);
console.log(shortMonthEn); // 示例输出:"May"
4. TC39 新一代原生标准:Temporal API
为了彻底解决包括 0 索引月份在内的多项 Date 历史顽疾,ECMAScript 标准委员会(TC39)提出了全新的 Temporal 顶层规范。在 Temporal 中,月份直接采用与自然认知完全一致的 1 ~ 12:
// 当前已在现代引擎演进,可使用 @js-temporal/polyfill
// 获取本地当前日期的纯日历对象
const today = Temporal.Now.plainDateISO();
console.log(today.month); // 直接返回 1 到 12 的自然月份,无需任何 +1 修正!
console.log(today.toString()); // 示例输出:"2026-05-18"
三、实际开发中的三大高频陷阱与防范方案
围绕 getMonth() 的特性,许多线上生产事故并不是因为开发者不知道加 1,而是因为忽略了运算符优先级、日期溢出机制和时区边界。
1. 陷阱一:字符串拼接中的运算符优先级(隐式转换)
在调试打印日志或拼接提示信息时,许多开发者习惯顺手使用 + 拼接字符串,这极易引发运算符优先级陷阱:
const d = new Date('2026-10-15'); // 10 月,此时 d.getMonth() 返回 9
// ❌ 错误写法:加号从左向右求值
console.log("当前月份:" + d.getMonth() + 1);
// 输出结果为:"当前月份:91"!
// 执行过程:先拼接为 "当前月份:9",再拼接 1 变成 "当前月份:91"
("当前月份:" + (d.getMonth() + 1)),或者直接使用 ES6 模板字符串:`当前月份:${d.getMonth() + 1}`。
2. 陷阱二:setMonth() 引发的月末跨月进位(高危生产事故)
这是在日期推算(例如“计算下个月今天的充值续费日”)中最为隐蔽的严重漏洞。例如在 1 月 31 日调用 setMonth(1) 尝试将月份设置为 2 月:
const date = new Date('2026-01-31');
// 试图修改为 2 月 (传入索引 1)
date.setMonth(1);
console.log(date.toISOString());
// 实际输出:2026-03-03T...
// 原因剖析:2026 年 2 月只有 28 天,设置目标为 2 月 31 日时,
// 超出的 3 天(31 - 28 = 3)会自动进位到 3 月 3 日!
setMonth() 进行月份调整前,必须先将日期重置为 1 号(date.setDate(1)),待安全完成月份设定后,再根据目标月份的实际最大天数(通过 new Date(year, month + 1, 0).getDate() 计算)设置回对应的日期。
3. 陷阱三:本地时区与 UTC 时间边界的不一致
getMonth() 返回的是运行该代码的操作系统当前时区下的月份。如果是在服务端(通常配置为 UTC 零时区)处理前端提交的时间戳,在月初跨日的那几个小时内,两者可能产生整整 1 个月的偏差:
- 例如北京时间(UTC+8)为
2026-06-01 03:00:00时,本地月份是 6 月(getMonth() === 5)。 - 但其对应的协调世界时为
2026-05-31 19:00:00 UTC,此时调用getUTCMonth()返回的是 5 月(getUTCMonth() === 4)。
在处理跨地域业务、生成分布式日志或服务端生成报表时,务必根据系统架构统一约定是使用本地时间方法(getMonth())还是 UTC 方法(getUTCMonth())。
四、现代技术选型与最佳实践总结
为了让不同场景下的选型更加直观清晰,下表归纳了当前主流方案的特性、形态与应用边界:
| 方案类型 | 核心代码形态 | 返回值类型 | 适用业务场景 | 主要优势与考量 |
|---|---|---|---|---|
| 基础偏移修正 | date.getMonth() + 1 |
Number (1~12) |
内部数值计算、季度推算、条件比较 | 零依赖、执行性能最高,但不含前导零 |
| 字符串补零 | String(m).padStart(2, '0') |
String ("01"~"12") |
接口入参拼接、标准日期字符串构造 | ES2017 原生规范,逻辑直观易懂 |
| 国际化 API | new Intl.DateTimeFormat(...) |
String (本地化文本) |
全球化多语言界面、文字月份展示 | 自动适配各国家语言习惯,无需自建字典 |
| TC39 Temporal | Temporal.Now.plainDateISO().month |
Number (1~12) |
现代化前沿项目、高可靠时间系统 | 原生 1-indexed,彻底解决 Date 历史设计缺陷 |
五、总结
Date.prototype.getMonth() 返回 0 到 11 并非现代前端架构的主动设计,而是 1995 年沿袭 C 语言底层结构与 Java 1.0 的历史遗产,并因 Web 平台的向后兼容性原则被永久固定。在实际开发中:
- 若需要获取数字自然月份,始终牢记加上偏移量:
const month = date.getMonth() + 1;。 - 若需两位标准格式,优先选用
String(date.getMonth() + 1).padStart(2, '0')。 - 在拼接字符串时注意圆括号优先级,在调整月份时谨防
setMonth()的月末溢出问题。 - 关注 TC39
Temporal新标准的发展,在未来技术架构中积极拥抱更加健壮的现代化时间模型。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
相关标签:








