JavaScript 内置了一个Date对象,用来存储日期和时间,并提供管理它们的方法。
它基于 Java 的java.util.Date类,这个类在 2010 年代初就被取代了,但出于向下兼容,JavaScript 的Date一直保留至今。
由于处理日期本身就十分困难,加上时区处理要么很糟糕、要么根本不存在,于是涌现出许多库,例如moment.js、day.js、date-fns和luxon。
这些库在 Exercism 上都不可用。
在你自己的项目里,不要使用moment.js这类已废弃、无人维护的包,而应依靠更现代的替代方案,比如luxon,或者尚未被广泛采用的 Temporal。
本练习聚焦于Date,它会一直重要到 JavaScript 的尽头。
Date对象是Date类的一个实例。
创建时可以不向构造函数传入任何实参。
这样得到的Date对象表示当前的日期和时间:
const now = new Date();
// => Thu Apr 14 2022 11:46:08 GMT+0530 (India Standard Time)
// Shows current day, date and time (in your time zone).
如果传入一个数字,它会被解释为timestamp。
时间戳是一个整数,表示自1970 年 1 月 1 日 UTC+0以来经过的毫秒数。
const epoch = new Date(0);
// Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)
const another = new Date(1749508766627);
// Tue Jun 10 2025 00:39:26 GMT+0200 (Central European Summer Time)
你可能会以为new Date(0)会生成“最早”的日期对象,但 JavaScript 会把日期转换到你所在的时区,这意味着只有在 GMT / UTC+0 时区附近的人,才能真正得到 Unix 纪元的值。
你可以向Date构造函数传入一个表示日期的字符串值。
在各种实现之间唯一一致的格式,是国际公认并标准化的所谓 ISO 8601 时间戳字符串的简化版本。
一个处于 UTC 的时刻看起来是这样的:
YYYY-MM-DDTHH:mm:ss.mssZ
YYYYMMDDTHHmmss.mssZ
其中的替换如下:
| 键 | 说明 | 默认值 |
|---|---|---|
| YYYY | 日历年,用 4 位数字表示 | |
| MM | 日历月,用 2 位数字表示 | 01 |
| DD | 日历日,用 2 位数字表示 | 01 |
| T | 字面字母 T,用来分隔日期和时间 | |
| HH | 24 小时制的小时,2 位数字 | 00 |
| mm | 分钟,2 位数字 | 00 |
| ss | 秒,2 位数字 | 00 |
| mss | 毫秒,3 位数字 | 000 |
| Z | 字面字母 Z,或偏移量+/-HH:mm
|
字面字母Z表示 UTC(无时区,无夏令时)。
由于大多数组成部分都有默认值,省略末尾的部分是合法的:
YYYY-MM-DD
时间默认为 00:00:00.000
如果时间戳不以Z结尾,也不以+HH:mm或-HH:mm(表示时区偏移量)结尾,那么出于历史原因,适用以下规则:
当缺少时区偏移量时,只有日期的形式会被解释为 UTC 时间,而日期加时间的形式会被解释为本地时间。 之所以解释为 UTC 时间,源于一个历史性的规范错误,它与 ISO 8601 不一致,但出于 Web 兼容性又无法更改。 参见 Broken Parser:一个 Web 现实问题。
Date.parse接受的其他格式可能能用,也可能不能用。
在 JavaScript 里处理日期时,从string转换成Date时_一定_要使用 ISO 8601 时间戳。
只含日期的形式是允许的,但并非所有 ISO 8601 格式都受支持。 请查阅 MDN 上的简化版本说明页面。
也可以把一个已有的日期对象作为构造函数的实参。
这会复制已有的Date对象,得到日期和时间都相同的一份副本。
const t1 = new Date();
const t2 = new Date(t1);
// Values of t1 and t2 will be the same.
传入三个数字,可以创建一个表示日期的日期。 传入 6 个数字,可以创建一个表示日期和时间的日期。
const date1 = new Date(95, 11, 17);
// Creates Date for Dec 17 1995 00:00 if your local timezone is equivalent to UTC.
const date2 = new Date(2013, 12, 5, 13, 24, 0);
// Creates Date for Jan 5 2014 13:24 if your local timezone is equivalent to UTC.
第二个值是month,一月从0开始,一直到十二月的11。
Date.parse()你可能会看到有人提到或引用一个日期解析函数Date.parse。
由于这个函数只有很少几条不变式(也就是真理),而其余实现都没有规定(因此也不是标准),所以不应该使用它。
Date的各个组成部分日期对象上有很多方法可以返回日期的各个组成部分:
getFullYear(); // Get the year (4 digits)
getMonth(); // Get the month, from 0 to 11.
getDate(); // Get the day of month, from 1 to 31.
getHours(); // Get the hour in a 24 clock, from 0 to 23
getMinutes(); // Get the minutes, from 0 to 59
getSeconds(); // Get the seconds, from 0 to 59
getMilliseconds(); // Get the milliseconds, from 0 and 999
getDay(); // Get the day of week, from 0 (Sunday) to 6 (Saturday).
这些方法每一个都有一个对应的set变体(例如setFullYear),用来更新值。
任何溢出的值都会进位到下一个组成部分:
const date = new Date('2025-02-28T12:42:00Z');
// => Fri Feb 28 2025 13:42:00 GMT+0100 (Central European Standard Time)
date.setDate(29);
// there was no February 29th in 2025.
date.getDate();
// => 1
date.toString();
// => Sat Mar 01 2025 13:42:00 GMT+0100 (Central European Standard Time)
所有这些方法都有忽略本地时区的 UTC 变体。
日期对象有一个方法getTime(),返回以毫秒为单位的 UNIX 时间戳,也就是从 1970 年 1 月 1 日 UTC 午夜开始经过的毫秒数。
此外,还有一个方法toISOString(),可以把日期对象转换成 ISO 8601 时间戳字符串。
大于(>)、大于或等于(>=)、小于(<)以及小于或等于(<=),都可以直接用在两个日期之间,或用在一个日期和一个数字之间。
这样之所以可行,是因为 JavaScript 会尝试把日期强制转换为原始值。
当在两个日期之间、或在一个日期和一个数字之间做比较时,JavaScript 会调用 [Symbol.toPrimitive]("number"),它内部又会调用 date.valueOf()。
后者等同于调用 date.getTime()。
如果你不想依赖这种行为,请先用getTime()转换成数字。
日期不能用相等比较(==和===),但.getTime()的结果可以。
在这个练习中,你将编写一些函数来管理预约。 系统把所有内容都存成 ISO 8601 格式的字符串,但人们使用日历时并不是这样。 要在这些格式之间相互转换,就需要用到各种函数。
创建一个从当前时刻起n天后的预约。
该函数接收n天,并返回从此刻起n天后的预约时间。
createAppointment(4, now);
// Given now is Sun Oct 05 2022 23:28:43 GMT+0600 (Bangladesh Standard Time)
// => Sun Oct 09 2022 23:28:43 GMT+0600 (Bangladesh Standard Time)
如果第二个形参now未被使用,则应改用当前时间。
许多工具只能处理国际标准的 ISO 8601 格式。
编写函数getAppointmentTimestamp,让它接收一个日期,并返回这种格式的字符串。
const appointment = new Date(Date.UTC(2010, 6, 16, 12, 42, 0, 0));
getAppointmentTimestamp(appointment);
// => '2010-07-16T12:42:00.000Z'
时间戳很难阅读,而一个用来获取预约详情的函数可以帮上忙。
函数getAppointmentDetails接收一个 ISO 8601 格式的时间戳,并返回年、月、日、时和分。
getAppointmentDetails('2022-04-24T08:15:00.000');
// => { year: 2022, month: 3, date: 24, hour: 8, minute: 15 }
该函数接收的第一个实参是预约时间,第二个实参是由若干选项组成的对象。 你需要根据对象中的选项更新预约,并返回新的预约日期。 选项对象中可能包含多个选项。
updateAppointment('2022-02-09T09:20:00.000', { month: 6 });
// => { year: 2022, month: 6, date: 9, hour: 10, minute: 20 }
该函数接收两个预约(时间戳)作为实参。 你需要以秒为单位返回这两个时间之差。
因为半秒几乎没有意义,所以在返回之前要先把数字四舍五入。
timeBetween('2022-12-12T09:20:00.000', '2022-12-18T08:30:00.000');
// => 515400
最后,预约创建之后,系统需要检查它是否有效。 换句话说,预约必须在将来,而不能在过去。
编写函数isValid,它接收两个实参:一个预约时间戳(字符串)和当前时间的时间戳(字符串);给定当前时间,如果预约在将来,则返回true。
isValid('2022-02-11T23:00:00.000', '2022-02-08T23:00:00.000');
// => true