1. 为什么 TypeScript 里的 Date 不是“日期”而是“时间戳容器”
刚接触 TypeScript 的开发者,常会把Date当成一个纯粹的“日期类型”——比如以为new Date('2024-03-15')就该稳稳地表示“2024年3月15日”,结果一打印.toISOString()却冒出'2024-03-14T16:00:00.000Z',瞬间懵了:怎么少了一天?这哪是“日期”,分明是个藏了时区陷阱的“时间戳容器”。
这不是你写错了,而是 TypeScript 对Date的建模方式,完全继承自 JavaScript 原生行为——它不提供独立的“日期”(date-only)或“时间”(time-only)类型,所有Date实例本质上都是毫秒级时间戳(number),附带一套基于本地时区或 UTC 的解析/格式化逻辑。TypeScript 的类型系统只是给这个 JS 原生对象加了一层静态接口描述(interface Date),但绝不改变其运行时行为。换句话说:TypeScript 的Date类型,是“对 JS Date 的类型注解”,不是“TS 自研的日期类型”。
这就导致一个关键矛盾:业务中大量存在“仅需日期”场景(如生日、合同生效日、报表统计日),但Date实例却强制携带时间与时区信息。你传入'2024-03-15',JS 引擎默认按本地时区解析为2024-03-15T00:00:00+08:00,再转成时间戳;而传入'2024-03-15T00:00:00'(无时区标识),则被当作 UTC 时间处理,结果在东八区就变成2024-03-14T16:00:00。这种差异不是 bug,是规范——ECMA-262 明确规定:ISO 格式字符串若无时区偏移,默认按 UTC 解析;而其他格式(如'2024/03/15')则按本地时区解析。
我第一次在电商后台做“订单创建日期筛选”时就栽在这儿。后端返回的是2024-03-15字符串,前端用new Date('2024-03-15')构造,结果在用户时区为 UTC+9 的日本客户那里,筛选出的却是 3 月 14 日的订单。查了三天,最后发现 Chrome 控制台里new Date('2024-03-15').toString()显示的是"Fri Mar 15 2024 00:00:00 GMT+0800 (中国标准时间)",而new Date('2024-03-15T00:00:00').toString()却是"Thu Mar 14 2024 16:00:00 GMT+0800 (中国标准时间)"——两个字符串长得几乎一样,行为却天差地别。这根本不是 TypeScript 的问题,而是我们误把“字符串输入”当成了“语义化日期输入”。
所以,理解Date的本质,是驾驭它的第一步:它不是日历上的一页纸,而是一根标着刻度的尺子,尺子本身没有“年月日”概念,只有“从 1970-01-01T00:00:00Z 开始的毫秒数”。所有.getFullYear()、.getDate()这些方法,都是拿这把尺子的刻度,套上某个时区的“读数规则”后算出来的结果。TypeScript 的作用,只是帮你提前发现“你调用了不存在的方法”(比如date.addDays(1)),而不是帮你绕过时区计算。
提示:不要试图用
Date类型去建模“纯日期”。如果你的业务字段明确只需要年月日(如身份证出生日期),最佳实践是使用string类型(格式固定为'YYYY-MM-DD'),或封装一个DateOnly类。强行用Date存储,等于在代码里埋下时区相关的定时炸弹。
2. 构造函数的七种写法与它们的真实含义
new Date()看似简单,实则暗藏七种构造路径,每种路径的解析逻辑、时区处理、甚至浏览器兼容性都不同。很多面试题(比如“new Date('2024-03-15')和new Date('2024/03/15')区别在哪?”)考的就是这个细节。下面我逐个拆解,不讲文档,只说实际跑出来的结果和底层逻辑。
2.1 无参构造:new Date()
这是最安全的用法——直接获取当前时刻的时间戳,并以本地时区为基准初始化。它不涉及任何字符串解析,纯属系统调用。生成的Date实例,其内部毫秒值等于Date.now()。所有后续的.get*()方法(如.getFullYear())返回的值,都是基于本地时区计算的。注意:new Date().toISOString()返回的是 UTC 时间字符串,因为.toISOString()规定必须输出 UTC。
2.2 时间戳构造:new Date(milliseconds)
传入一个数字,代表从 Unix Epoch(1970-01-01T00:00:00Z)开始的毫秒数。这是唯一一种完全规避时区解析风险的方式。例如new Date(1710489600000)在全球任何地方都精确对应2024-03-15T00:00:00.000Z。但缺点是可读性极差,你需要自己计算毫秒值,或者依赖Date.parse()转换。实际项目中,我只在处理 WebSocket 二进制协议或高性能日志时间戳时才直接用这个。
2.3 年月日时分秒构造:new Date(year, monthIndex, date?, hours?, minutes?, seconds?, ms?)
这是最可控的构造方式,但有个致命陷阱:monthIndex是从 0 开始的(0 表示一月,11 表示十二月)。new Date(2024, 2, 15)才是 2024 年 3 月 15 日,而非new Date(2024, 3, 15)。这个设计源于 Java 的Calendar类,JS 继承了它。TypeScript 的类型定义declare function Date(year: number, month: number, ...): Date;并不会阻止你传入month=12,它只会让month参数类型为number,而不会做运行时校验。所以,我习惯写一个包装函数:
function createDate(year: number, month: number, date: number = 1, hour: number = 0, minute: number = 0, second: number = 0, millisecond: number = 0): Date { // 自动修正 month:用户输入 1~12,内部转为 0~11 const monthIndex = Math.max(0, Math.min(11, month - 1)); return new Date(year, monthIndex, date, hour, minute, second, millisecond); } // 使用:createDate(2024, 3, 15) → 2024-03-152.4 ISO 8601 字符串构造:new Date('YYYY-MM-DDTHH:mm:ss.sssZ')
这是标准最严格的格式。带Z(如'2024-03-15T00:00:00.000Z')表示 UTC 时间,解析结果在任何时区都一致。带+08:00(如'2024-03-15T00:00:00.000+08:00')则明确指定时区偏移。关键点在于:如果字符串符合 ISO 格式且包含时区信息,所有现代浏览器解析结果一致。这也是为什么后端 API 应该统一返回带Z或时区偏移的 ISO 字符串,而不是裸'2024-03-15'。
2.5 本地格式字符串构造:new Date('2024/03/15')或new Date('Mar 15, 2024')
这类字符串不遵循 ISO 标准,解析行为由宿主环境(浏览器/Node.js)决定,且没有统一规范。Chrome 和 Firefox 对'2024/03/15'的解析基本一致(按本地时区),但对'15/03/2024'(日/月/年格式)就可能失败或返回Invalid Date。Safari 在某些旧版本中对'2024-03-15'(看似 ISO)也会按本地时区解析,而非 UTC,造成跨浏览器差异。因此,绝对不要依赖此类字符串构造。我在尚硅谷的 TypeScript 课件笔记里看到有学员用'2024-03-15'初始化,结果在 Safari 上出错,根源就在此。
2.6 Unix Timestamp 字符串:new Date('1710489600000')
传入纯数字字符串,会被Date构造函数隐式转换为数字,等价于new Date(1710489600000)。这看起来很取巧,但存在风险:如果字符串里混入空格或非数字字符(如' 1710489600000 '),parseInt会截断,导致毫秒值错误。而且,可读性为零。我见过有团队用这种方式存“未来时间”,结果某次 CI 构建时因环境变量注入了额外空格,导致所有定时任务提前 10 年触发。
2.7 其他格式字符串:new Date('Fri Mar 15 2024')
这是Date.prototype.toString()的逆向操作,但完全不可靠。不同浏览器、不同语言环境(locale)下,toString()输出格式不同,new Date()能否正确解析也不同。Node.js 的Date构造函数对此类字符串的支持远弱于浏览器。把它当成“能用就行”的方案,迟早会在某个生产环境里崩掉。
总结一下我的选型经验:
- 需要当前时间 → 用
new Date(); - 需要精确到毫秒的已知时间点 → 用
new Date(timestamp); - 需要构造一个具体日期(年月日)→ 用
new Date(year, monthIndex, date)并自行封装month校验; - 接收后端数据 →只接受带
Z或+00:00的 ISO 字符串,并用new Date(isoString); - 其他所有字符串格式 → 统一拒绝,抛出错误或降级为
new Date()。
3. getFullYear() 与 getYear():一个被废弃的远古遗民
Date.prototype.getYear()是 JavaScript 1.0 时代的产物,设计初衷是为了节省字节——它返回的是“年份减去 1900”的值。比如new Date(1999, 0, 1).getYear()返回99,new Date(2024, 0, 1).getYear()返回124。这在 Y2K 问题爆发后就被视为重大缺陷,ECMAScript 3(1999)起就将其标记为废弃(deprecated),推荐使用getFullYear()。但诡异的是,所有浏览器至今仍保留这个方法,且 TypeScript 的lib.es5.d.ts里依然声明了它:
interface Date { getYear(): number; // ❌ 仍在类型定义里,但文档明确说“不要用” }这就造成了一个隐蔽的坑:TypeScript 编译器不会报错,IDE 会提示这个方法存在,但你一旦用了,代码就背离了现代 JS 最佳实践。我在一个老项目里重构时,发现十几处date.getYear() + 1900的写法,问原作者,他说“IDE 有提示,就用了”。这恰恰说明:类型系统不能替代知识更新。TypeScript 的类型定义是向后兼容的,它包含了所有历史遗留 API,哪怕它们已被废弃十年。
更麻烦的是getYear()的行为在不同年份有差异:对于 1900–1999 年间的日期,它返回两位数(如 1995 → 95);对于 2000 年之后,它返回四位数减 1900(如 2024 → 124)。这意味着date.getYear() < 100这种判断,在 2000 年后永远为false,逻辑彻底失效。而getFullYear()则始终返回四位数年份,稳定可靠。
所以,我的硬性规定是:在 ESLint 配置中启用no-restricted-properties规则,禁止使用getYear、setYear、toGMTString(已废弃,应使用toUTCString)等方法。同时,在团队代码规范里明确写出:“Date实例的年份操作,只允许使用getFullYear()/setFullYear()”。
另一个常被混淆的是getDate()和getDay():
getDate()返回月份中的第几天(1–31),对应日历上的“日期”;getDay()返回星期几(0–6,0 是周日,6 是周六),对应“星期”。
中文里“日期”一词常指代“年月日”整体,但getDay()的命名是历史包袱,它和“日期”毫无关系。我教新人时,会让他们立刻记住口诀:“getDate是‘号’,getDay是‘星期’”。
注意:
getMonth()返回 0–11,getDate()返回 1–31,getDay()返回 0–6 —— 这三个方法的返回范围完全不同,切勿记混。我曾见有同事写if (date.getDay() === 0)判断是否为“第一天”,结果逻辑全乱。
4. toISOString() 与 toString():同一时间,两种世界观
Date实例的字符串化方法,是时区问题最直观的暴露口。toISOString()和toString()的输出差异,完美体现了“UTC 时间观”与“本地时间观”的根本对立。
4.1 toISOString():UTC 时间的标准化表达
toISOString()的行为极其确定:它总是返回 UTC 时间的 ISO 8601 字符串,格式为'YYYY-MM-DDTHH:mm:ss.sssZ'。无论你的系统时区是+08:00还是-05:00,new Date('2024-03-15T00:00:00').toISOString()永远是'2024-03-15T00:00:00.000Z'。这是因为toISOString()内部先调用getTime()获取毫秒时间戳,再将该时间戳转换为 UTC 时间进行格式化。它是跨时区通信的黄金标准,后端 API、数据库存储、日志记录,都应该使用这个格式。
但这里有个经典误区:有人以为toISOString()“修复”了时区问题。其实不然。假设你在北京时间(UTC+8)下午 4 点执行new Date().toISOString(),得到的是'2024-03-15T08:00:00.000Z'(即 UTC 时间 8 点),这恰恰证明了Date实例的毫秒值是固定的,toISOString()只是换了一种方式读它。它没有改变Date本身,只是提供了另一种视角。
4.2 toString():本地时区的“人性化”表达
toString()的输出则完全取决于运行环境的本地时区设置。在北京执行new Date('2024-03-15T00:00:00Z').toString(),会得到'Fri Mar 15 2024 08:00:00 GMT+0800 (中国标准时间)';在纽约执行,会得到'Fri Mar 15 2024 03:00:00 GMT-0500 (北美东部标准时间)'。它把同一个毫秒时间戳,映射到了本地时区的日历系统上,目的是让人眼可读。但正因如此,它绝不能用于序列化或传输——接收方无法知道这个字符串是基于哪个时区生成的。
有趣的是,toLocaleString()系列方法(toLocaleDateString()、toLocaleTimeString())比toString()更进一步:它们不仅考虑时区,还考虑 locale(语言区域)。new Date('2024-03-15').toLocaleDateString('zh-CN')返回'2024年3月15日',而new Date('2024-03-15').toLocaleDateString('en-US')返回'3/15/2024'。这在国际化应用中至关重要,但同样,它只适用于 UI 层展示,不能用于计算或存储。
4.3 实战对比:一个订单创建时间的完整链路
假设后端返回订单时间为'2024-03-15T00:00:00Z'(UTC),前端需要:
- 存储:用
toISOString()保持原始精度; - 展示给北京用户:用
toLocaleDateString('zh-CN')显示“2024年3月15日”; - 展示给纽约用户:用
toLocaleDateString('en-US')显示“3/15/2024”; - 计算“距今多少天”:用
Date.now() - date.getTime()得到毫秒差,再除以1000 * 60 * 60 * 24。
错误做法是:const date = new Date('2024-03-15T00:00:00Z'); console.log(date.toString());—— 这个toString()输出的字符串,包含了时区信息,但如果你把它再传回后端,后端很可能无法解析(尤其当后端是 Java 或 Python,对时区字符串支持不一)。
正确做法是:
const serverIsoString = '2024-03-15T00:00:00Z'; const date = new Date(serverIsoString); // 构造时已按 UTC 解析 // ✅ 安全存储/传输:保持 ISO 格式 const isoForBackend = date.toISOString(); // '2024-03-15T00:00:00.000Z' // ✅ 安全展示:用 toLocaleXXX const displayDate = date.toLocaleDateString('zh-CN'); // '2024年3月15日' // ✅ 安全计算:用 getTime() const daysAgo = Math.floor((Date.now() - date.getTime()) / (1000 * 60 * 60 * 24));这个链路里,toISOString()是“信使”,toLocaleDateString()是“翻译官”,getTime()是“计算器”。三者各司其职,混用就会出错。
5. 日期计算的三大雷区与我的防御式封装
用Date做加减运算,表面看很简单:date.setDate(date.getDate() + 1)就是加一天。但实际踩过的坑,足够写一本《Date 事故汇编》。下面我直击最痛的三个雷区,并给出经过生产验证的防御式封装方案。
5.1 雷区一:月份边界溢出——2 月 31 日自动变成 3 月 2 日?
这是Date最反直觉的行为。new Date(2024, 1, 31)(2024 年 2 月 31 日)不会报错,而是自动“归位”为2024-03-02(3 月 2 日)。因为Date构造函数和 setter 方法(如setDate())都具备自动归一化(normalization)能力:当你设置一个超出范围的日期值时,它会调整月份、年份等其他字段来“消化”这个溢出。
这听起来很智能,但会引发严重逻辑错误。比如,一个“每月 31 号提醒”的功能,如果用户在 2 月设置,setDate(31)后变成 3 月 2 号,提醒就永远错位。我曾在 NestJS 项目里写了一个定时任务调度器,用date.setDate(date.getDate() + 30)计算下月同日,结果在 1 月 31 日执行后,变成了 3 月 2 日,整个调度链崩了。
防御方案:永远不要依赖自动归一化做业务逻辑。正确做法是:先获取当前日期对象,再用setMonth()和setDate()组合,手动处理边界。
// ✅ 安全的“加一个月”函数 function addMonths(date: Date, months: number): Date { const result = new Date(date); const currentMonth = result.getMonth(); const currentYear = result.getFullYear(); // 先加月份,不碰日期 result.setMonth(currentMonth + months); // 获取新月份的最大天数 const maxDaysInNewMonth = new Date(result.getFullYear(), result.getMonth() + 1, 0).getDate(); // 如果原日期超过新月份最大天数,则设为最后一天 if (date.getDate() > maxDaysInNewMonth) { result.setDate(maxDaysInNewMonth); } return result; } // 测试:2024-01-31 加一个月 → 2024-02-29(闰年) console.log(addMonths(new Date(2024, 0, 31), 1)); // 2024-02-29 // 测试:2023-01-31 加一个月 → 2023-02-28(平年) console.log(addMonths(new Date(2023, 0, 31), 1)); // 2023-02-285.2 雷区二:夏令时跳跃——凌晨 2 点直接变成 3 点,加一小时变负一小时?
在实行夏令时的地区(如美国、欧盟),每年春秋季时钟会跳变。Spring Forward(春调)时,凌晨 2:00 直接跳到 3:00,中间消失一小时;Fall Back(秋调)时,凌晨 2:00 重复一次,多出一小时。Date对象在处理这个区间时,行为高度依赖宿主环境的时区数据库(ICU),且不同浏览器版本可能不同。
最典型的坑是:new Date('2023-11-05T01:30:00').getTime() + 3600000(加一小时),在秋调当天,可能得到2023-11-05T01:30:00(因为 2:00 出现两次,加一小时后还是 1:30?),也可能得到2023-11-05T02:30:00(第二次 2:00)。结果完全不确定。
防御方案:避开本地时区做时间计算。所有加减运算,统一转换为 UTC 时间戳进行。Date的毫秒值是绝对的,不受夏令时影响。date.getTime() + n * 1000 * 60 * 60永远是可靠的。
// ✅ 安全的“加 N 小时”函数(无视夏令时) function addHours(date: Date, hours: number): Date { const timestamp = date.getTime(); const newTimestamp = timestamp + hours * 60 * 60 * 1000; return new Date(newTimestamp); } // 测试:在秋调日(2023-11-05)加 1 小时 const beforeDST = new Date('2023-11-05T01:30:00'); // 第一次 1:30 console.log(addHours(beforeDST, 1).toISOString()); // '2023-11-05T06:30:00.000Z'(UTC 时间,绝对正确)5.3 雷区三:跨时区比较——用==比较两个Date实例?
Date实例是对象,date1 == date2比较的是引用,而非时间值。date1.getTime() === date2.getTime()才是正确的相等判断。但更隐蔽的坑是:date1 > date2这种比较,在大多数情况下是可行的,因为Date的valueOf()方法返回毫秒时间戳,比较运算符会自动调用它。然而,一旦涉及Date与其他类型的混合比较(如date > '2024-03-15'),结果就完全不可预测,因为字符串会被强制转换为NaN,比较结果为false。
防御方案:所有比较操作,显式调用getTime()。我在 LayaUI 项目里曾遇到一个 bug:if (orderDate > deadline)总是false,排查发现deadline是字符串'2024-03-15',而orderDate是Date实例。JS 引擎尝试把字符串转为数字失败,'2024-03-15'变成NaN,任何数与NaN比较都为false。
最终,我封装了一个DateUtils工具类,强制所有日期操作走安全路径:
class DateUtils { static isSameDay(d1: Date, d2: Date): boolean { return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); } static isBefore(d1: Date, d2: Date): boolean { return d1.getTime() < d2.getTime(); } static isAfter(d1: Date, d2: Date): boolean { return d1.getTime() > d2.getTime(); } static addDays(date: Date, days: number): Date { return new Date(date.getTime() + days * 24 * 60 * 60 * 1000); } // ... 其他方法 } // 使用:DateUtils.isBefore(orderDate, deadlineDate)这个类的存在,不是为了炫技,而是为了在团队里建立一条“安全红线”:只要用DateUtils,就不可能写出时区或比较相关的 bug。
6. TypeScript 类型层面的终极补救:DateOnly 与 DateTime 的分离建模
TypeScript 的核心价值,在于用类型约束提前捕获错误。既然原生Date无法区分“纯日期”和“日期时间”,我们就该在类型层面主动隔离它们。这不是“过度设计”,而是对业务语义的尊重。下面是我基于多年 NestJS 和 TypeScript 项目经验,提炼出的两套成熟方案。
6.1 方案一:字符串字面量类型(轻量级,零运行时开销)
对于“纯日期”字段(如生日、入职日、合同日期),最简单有效的方案是放弃Date,改用string类型,并用字符串字面量类型约束格式:
type DateOnly = `${number}-${number}-${number}`; // 粗略约束 // 更严格的约束(需 TypeScript 4.5+) type Year = `${number}`; type Month = '01' | '02' | '03' | '04' | '05' | '06' | '07' | '08' | '09' | '10' | '11' | '12'; type Day = '01' | '02' | /* ... */ | '31'; type DateOnly = `${Year}-${Month}-${Day}`; // 使用 interface User { name: string; birthday: DateOnly; // "1990-05-20" } // ✅ 编译期检查:'1990-13-20' 会报错,'1990-05-20' 通过 const user: User = { name: 'Alice', birthday: '1990-05-20' };优点:零运行时成本,类型精准,与 JSON 序列化天然兼容(API 传'2024-03-15'字符串即可)。缺点:无法进行日期计算(如“加一天”),需要额外工具函数处理。
6.2 方案二:不可变值对象(重量级,强语义保障)
对于需要频繁计算的场景(如财务系统、排班系统),我推荐封装一个DateTime类,它内部仍用Date,但对外屏蔽所有危险操作,并强制时区意识:
class DateTime { private readonly _date: Date; private constructor(date: Date) { this._date = date; } // ✅ 强制指定时区来源:只能从 ISO 字符串(含 Z)或时间戳创建 static fromISOString(isoString: string): DateTime { if (!isoString.endsWith('Z')) { throw new Error('ISO string must end with Z for UTC'); } return new DateTime(new Date(isoString)); } static fromTimestamp(timestamp: number): DateTime { return new DateTime(new Date(timestamp)); } // ✅ 安全的加减法 addDays(days: number): DateTime { return new DateTime(new Date(this._date.getTime() + days * 24 * 60 * 60 * 1000)); } // ✅ 安全的比较 isBefore(other: DateTime): boolean { return this._date.getTime() < other._date.getTime(); } // ✅ 安全的序列化 toISOString(): string { return this._date.toISOString(); } // ✅ 安全的本地化展示(需传入 locale) toLocaleDateString(locale: string): string { return this._date.toLocaleDateString(locale); } } // 使用 const now = DateTime.fromISOString(new Date().toISOString()); const tomorrow = now.addDays(1); console.log(tomorrow.toISOString()); // 安全 console.log(tomorrow.toLocaleDateString('zh-CN')); // 安全这个DateTime类,本质上是一个“类型防火墙”。它不允许你用new Date('2024-03-15')这种危险方式构造,也不允许你调用getYear()这类废弃方法。所有方法都经过审查,确保行为可预测。在 NestJS 项目中,我把它作为 DTO 的属性类型,配合 class-validator,实现了从 API 输入到业务逻辑的全程类型安全。
6.3 方案三:第三方库集成(务实选择)
如果项目允许引入依赖,date-fns是目前最符合 TypeScript 哲学的日期库。它的所有函数都是纯函数,不修改原Date实例,且类型定义极其完善:
npm install date-fns npm install @types/date-fnsimport { addDays, format, parseISO, isSameDay } from 'date-fns'; // ✅ parseISO 严格解析 ISO 字符串,失败返回 Invalid Date const date = parseISO('2024-03-15T00:00:00Z'); // ✅ addDays 返回新 Date,不修改原实例 const tomorrow = addDays(date, 1); // ✅ format 强制指定 locale 和格式,无歧义 const display = format(tomorrow, 'yyyy年MM月dd日', { locale: zhCN }); // ✅ isSameDay 比较两天是否为同一天(忽略时间) const sameDay = isSameDay(date, tomorrow); // falsedate-fns的优势在于:它把所有日期操作都显式化、函数化,彻底规避了Date原型方法的副作用和时区陷阱。它的 TypeScript 类型定义由社区维护,与最新 TS 版本同步,format函数的格式字符串甚至有类型提示。在“三小时快速上手 TypeScript”课件里,我总建议学员:学完基础类型后,立刻用date-fns实践,因为它能把抽象的类型理论,落地为具体的、可触摸的安全代码。
提示:不要用
moment.js。它已被官方标记为 legacy,体积大,mutable API 易出错,且 TypeScript 支持不如date-fns。dayjs是轻量替代,但date-fns的 tree-shaking 和类型体验更胜一筹。
7. 面试高频题实战拆解:从“如何判断今天是周几”到“实现一个日期范围选择器”
TypeScript 面试中,Date相关题目从来不是考你会不会写new Date(),而是考你能否识别陷阱、设计防御、权衡取舍。下面我用两道真实高频题,还原面试官的考察意图和我的作答思路。
7.1 题目一:“写一个函数,判断今天是星期几,返回中文(周一、周二…)”
表面看是送分题,但陷阱层层嵌套:
// ❌ 错