2022 年 10 月 14 日

日期和时间

让我们认识一个新的内置对象:日期。它存储日期、时间,并提供日期/时间管理方法。

例如,我们可以用它来存储创建/修改时间,来测量时间,或者只是打印出当前日期。

创建

要创建一个新的日期对象,请使用以下参数之一调用new Date()

new Date()

无参数 – 为当前日期和时间创建一个日期对象

let now = new Date();
alert( now ); // shows current date/time
new Date(毫秒)

使用从 1970 年 1 月 1 日 UTC+0 开始经过的毫秒数(1/1000 秒)创建一个 Date 对象。

// 0 means 01.01.1970 UTC+0
let Jan01_1970 = new Date(0);
alert( Jan01_1970 );

// now add 24 hours, get 02.01.1970 UTC+0
let Jan02_1970 = new Date(24 * 3600 * 1000);
alert( Jan02_1970 );

表示自 1970 年开始经过的毫秒数的整数称为时间戳

它是日期的轻量级数字表示形式。我们始终可以使用 new Date(timestamp) 从时间戳创建日期,并使用 date.getTime() 方法将现有的 Date 对象转换为时间戳(见下文)。

1970 年 1 月 1 日之前的时间戳为负数,例如

// 31 Dec 1969
let Dec31_1969 = new Date(-24 * 3600 * 1000);
alert( Dec31_1969 );
new Date(datestring)

如果只有一个参数,并且它是一个字符串,那么它将被自动解析。算法与 Date.parse 使用的算法相同,我们稍后会介绍。

let date = new Date("2017-01-26");
alert(date);
// The time is not set, so it's assumed to be midnight GMT and
// is adjusted according to the timezone the code is run in
// So the result could be
// Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time)
// or
// Wed Jan 25 2017 16:00:00 GMT-0800 (Pacific Standard Time)
new Date(year, month, date, hours, minutes, seconds, ms)

使用给定的组件在本地时区创建日期。只有前两个参数是必需的。

  • year 应有 4 位数字。为了兼容性,还接受 2 位数字并将其视为 19xx,例如,此处 981998 相同,但始终强烈建议使用 4 位数字。
  • month 计数从 0(1 月)开始,到 11(12 月)结束。
  • date 参数实际上是该月的日期,如果不存在,则假定为 1
  • 如果 hours/minutes/seconds/ms 不存在,则假定它们等于 0

例如

new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00
new Date(2011, 0, 1); // the same, hours etc are 0 by default

最大精度为 1 毫秒(1/1000 秒)

let date = new Date(2011, 0, 1, 2, 3, 4, 567);
alert( date ); // 1.01.2011, 02:03:04.567

访问日期组件

有一些方法可以从 Date 对象访问年、月等

getFullYear()
获取年份(4 位数字)
getMonth()
获取月份,从 0 到 11
getDate()
获取该月的日期,从 1 到 31,该方法的名称看起来有点奇怪。
getHours()getMinutes()getSeconds()getMilliseconds()
获取相应的时间组件。
不是 getYear(),而是 getFullYear()

许多 JavaScript 引擎实现了非标准方法 getYear()。此方法已弃用。它有时会返回 2 位数的年份。请不要使用它。年份可以使用 getFullYear()

此外,我们可以获取星期几

getDay()
获取星期几,从 0(星期日)到 6(星期六)。第一天始终是星期日,在某些国家并非如此,但无法更改。

上述所有方法都返回相对于本地时区的组件。

还有它们的 UTC 对应项,它们返回 UTC+0 时区的日期、月份、年份等:getUTCFullYear()getUTCMonth()getUTCDay()。只需在 "get" 后插入 "UTC" 即可。

如果你的本地时区相对于 UTC 有偏移,那么下面的代码将显示不同的小时

// current date
let date = new Date();

// the hour in your current time zone
alert( date.getHours() );

// the hour in UTC+0 time zone (London time without daylight savings)
alert( date.getUTCHours() );

除了给定的方法外,还有两个特殊的方法没有 UTC 变体

getTime()

返回日期的时间戳 - 自 1970 年 1 月 1 日 UTC+0 起经过的毫秒数。

getTimezoneOffset()

返回 UTC 和本地时区之间的差值(以分钟为单位)

// if you are in timezone UTC-1, outputs 60
// if you are in timezone UTC+3, outputs -180
alert( new Date().getTimezoneOffset() );

设置日期组件

以下方法允许设置日期/时间组件

除了 setTime() 之外,它们中的每一个都有一个 UTC 变体,例如:setUTCHours()

正如我们所看到的,一些方法可以一次设置多个组件,例如 setHours。未提及的组件不会被修改。

例如

let today = new Date();

today.setHours(0);
alert(today); // still today, but the hour is changed to 0

today.setHours(0, 0, 0, 0);
alert(today); // still today, now 00:00:00 sharp.

自动更正

自动更正Date 对象非常方便的一个特性。我们可以设置超出范围的值,它会自动调整自身。

例如

let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!?
alert(date); // ...is 1st Feb 2013!

超出范围的日期组件会自动分配。

假设我们需要将日期“2016 年 2 月 28 日”增加 2 天。如果是闰年,则可能是“3 月 2 日”或“3 月 1 日”。我们不必考虑它。只需添加 2 天。Date 对象将完成其余的工作

let date = new Date(2016, 1, 28);
date.setDate(date.getDate() + 2);

alert( date ); // 1 Mar 2016

该功能通常用于获取给定时间段之后的日期。例如,让我们获取“现在之后的 70 秒”的日期

let date = new Date();
date.setSeconds(date.getSeconds() + 70);

alert( date ); // shows the correct date

我们还可以设置零甚至负值。例如

let date = new Date(2016, 0, 2); // 2 Jan 2016

date.setDate(1); // set day 1 of month
alert( date );

date.setDate(0); // min day is 1, so the last day of the previous month is assumed
alert( date ); // 31 Dec 2015

日期到数字,日期差异

Date 对象转换为数字时,它将成为与 date.getTime() 相同的时间戳

let date = new Date();
alert(+date); // the number of milliseconds, same as date.getTime()

重要的副作用:日期可以相减,结果是它们以毫秒为单位的差值。

这可用于时间测量

let start = new Date(); // start measuring time

// do the job
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}

let end = new Date(); // end measuring time

alert( `The loop took ${end - start} ms` );

Date.now()

如果我们只想测量时间,则不需要 Date 对象。

有一个特殊方法 Date.now(),它返回当前时间戳。

它在语义上等同于 new Date().getTime(),但它不会创建中间 Date 对象。因此,它更快并且不会给垃圾回收施加压力。

它主要用于方便或在性能至关重要的情况下,例如 JavaScript 游戏或其他专门应用程序。

所以这可能更好

let start = Date.now(); // milliseconds count from 1 Jan 1970

// do the job
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}

let end = Date.now(); // done

alert( `The loop took ${end - start} ms` ); // subtract numbers, not dates

基准测试

如果我们想要对 CPU 密集型函数进行可靠的基准测试,我们应该小心。

例如,让我们测量计算两个日期之间差值的两个函数:哪个更快?

此类性能测量通常称为“基准测试”。

// we have date1 and date2, which function faster returns their difference in ms?
function diffSubtract(date1, date2) {
  return date2 - date1;
}

// or
function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

这两个函数执行完全相同的事情,但其中一个使用显式 date.getTime() 以毫秒为单位获取日期,而另一个则依赖于日期到数字的转换。它们的结果始终相同。

那么,哪一个更快?

第一个想法可能是连续多次运行它们并测量时间差。对于我们的情况,函数非常简单,因此我们必须至少执行 100000 次。

让我们测量

function diffSubtract(date1, date2) {
  return date2 - date1;
}

function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

function bench(f) {
  let date1 = new Date(0);
  let date2 = new Date();

  let start = Date.now();
  for (let i = 0; i < 100000; i++) f(date1, date2);
  return Date.now() - start;
}

alert( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' );
alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' );

哇!使用 getTime() 快多了!这是因为没有类型转换,引擎更容易优化。

好的,我们有了一些东西。但这还不是一个好的基准测试。

想象一下,在运行 bench(diffSubtract) 时,CPU 正在并行执行某些操作,并且正在占用资源。而在运行 bench(diffGetTime) 时,该操作已完成。

对于现代多进程操作系统而言,这是一个非常真实的场景。

因此,第一个基准测试的 CPU 资源将少于第二个基准测试。这可能会导致错误的结果。

为了获得更可靠的基准测试,应多次重新运行整个基准测试包。

例如,如下所示

function diffSubtract(date1, date2) {
  return date2 - date1;
}

function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

function bench(f) {
  let date1 = new Date(0);
  let date2 = new Date();

  let start = Date.now();
  for (let i = 0; i < 100000; i++) f(date1, date2);
  return Date.now() - start;
}

let time1 = 0;
let time2 = 0;

// run bench(diffSubtract) and bench(diffGetTime) each 10 times alternating
for (let i = 0; i < 10; i++) {
  time1 += bench(diffSubtract);
  time2 += bench(diffGetTime);
}

alert( 'Total time for diffSubtract: ' + time1 );
alert( 'Total time for diffGetTime: ' + time2 );

现代 JavaScript 引擎仅对执行多次的“热点代码”应用高级优化(无需优化很少执行的操作)。因此,在上面的示例中,首次执行并未得到很好的优化。我们可能需要添加预热运行

// added for "heating up" prior to the main loop
bench(diffSubtract);
bench(diffGetTime);

// now benchmark
for (let i = 0; i < 10; i++) {
  time1 += bench(diffSubtract);
  time2 += bench(diffGetTime);
}
小心进行微基准测试

现代 JavaScript 引擎执行许多优化。与“正常使用”相比,它们可能会调整“人工测试”的结果,尤其是在我们对非常小的内容(例如运算符的工作方式或内置函数)进行基准测试时。因此,如果您真的想了解性能,请研究 JavaScript 引擎的工作原理。然后,您可能根本不需要微基准测试。

有关 V8 的大量文章可以在 https://mrale.ph 找到。

从字符串解析 Date.parse

方法 Date.parse(str) 可以从字符串中读取日期。

字符串格式应为:YYYY-MM-DDTHH:mm:ss.sssZ,其中

  • YYYY-MM-DD – 是日期:年-月-日。
  • 字符 "T" 用作分隔符。
  • HH:mm:ss.sss – 是时间:小时、分钟、秒和毫秒。
  • 可选的 'Z' 部分表示时区,格式为 +-hh:mm。单个字母 Z 表示 UTC+0。

也可以使用较短的变体,例如 YYYY-MM-DDYYYY-MM 甚至 YYYY

Date.parse(str) 的调用以给定格式解析字符串,并返回时间戳(从 1970 年 1 月 1 日 UTC+0 开始的毫秒数)。如果格式无效,则返回 NaN

例如

let ms = Date.parse('2012-01-26T13:51:50.417-07:00');

alert(ms); // 1327611110417  (timestamp)

我们可以立即从时间戳创建 new Date 对象

let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') );

alert(date);

摘要

  • JavaScript 中的日期和时间用 Date 对象表示。我们无法创建“仅日期”或“仅时间”:Date 对象始终同时包含两者。
  • 月份从零开始计数(是的,一月是零月)。
  • getDay() 中的星期几也从零开始计数(即星期日)。
  • 当组件超出范围时,Date 会自动更正。适合于增加/减少天数/月数/小时数。
  • 可以对日期进行减法运算,得到它们以毫秒为单位的差值。这是因为将 Date 转换为数字时,它会变成时间戳。
  • 使用 Date.now() 快速获取当前时间戳。

请注意,与许多其他系统不同,JavaScript 中的时间戳以毫秒为单位,而不是秒。

有时我们需要更精确的时间测量。JavaScript 本身没有办法以微秒(百万分之一秒)为单位测量时间,但大多数环境都提供了这种方法。例如,浏览器具有 performance.now(),它提供从页面加载开始到当前时间以毫秒为单位的时间,精度为微秒(小数点后 3 位数字)

alert(`Loading started ${performance.now()}ms ago`);
// Something like: "Loading started 34731.26000000001ms ago"
// .26 is microseconds (260 microseconds)
// more than 3 digits after the decimal point are precision errors, only the first 3 are correct

Node.js 有 microtime 模块和其他方法。从技术上讲,几乎任何设备和环境都可以获得更高的精度,只是不在 Date 中。

任务

重要性:5

为日期创建 Date 对象:2012 年 2 月 20 日,凌晨 3:12。时区为当地时区。

使用 alert 显示它。

new Date 构造函数使用当地时区。因此,唯一需要记住的重要事项是月份从零开始。

因此,2 月份的数字为 1。

下面是一个以数字作为日期组件的示例

//new Date(year, month, date, hour, minute, second, millisecond)
let d1 = new Date(2012, 1, 20, 3, 12);
alert( d1 );

我们还可以从字符串创建日期,如下所示

//new Date(datastring)
let d2 = new Date("2012-02-20T03:12");
alert( d2 );
重要性:5

编写一个函数 getWeekDay(date) 以短格式显示星期几:‘MO’、‘TU’、‘WE’、‘TH’、‘FR’、‘SA’、‘SU’。

例如

let date = new Date(2012, 0, 3);  // 3 Jan 2012
alert( getWeekDay(date) );        // should output "TU"

打开一个带有测试的沙盒。

方法 date.getDay() 返回星期几的数字,从星期日开始。

让我们创建一个星期几的数组,以便我们可以通过数字获取正确的星期几名称

function getWeekDay(date) {
  let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];

  return days[date.getDay()];
}

let date = new Date(2014, 0, 3); // 3 Jan 2014
alert( getWeekDay(date) ); // FR

在沙盒中打开带有测试的解决方案。

重要性:5

欧洲国家的一周从星期一(数字 1)开始,然后是星期二(数字 2),一直到星期日(数字 7)。编写一个函数 getLocalDay(date),为 date 返回“欧洲”星期几。

let date = new Date(2012, 0, 3);  // 3 Jan 2012
alert( getLocalDay(date) );       // tuesday, should show 2

打开一个带有测试的沙盒。

function getLocalDay(date) {

  let day = date.getDay();

  if (day == 0) { // weekday 0 (sunday) is 7 in european
    day = 7;
  }

  return day;
}

在沙盒中打开带有测试的解决方案。

重要性:4

创建一个函数 getDateAgo(date, days),以从 date 中返回 days 天前的日期。

例如,如果今天是 20 日,那么 getDateAgo(new Date(), 1) 应为 19 日,而 getDateAgo(new Date(), 2) 应为 18 日。

对于 days=365 或更多天,应可靠地工作

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015)
alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014)
alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014)

P.S. 该函数不应修改给定的 date

打开一个带有测试的沙盒。

这个想法很简单:从 date 中减去给定的天数

function getDateAgo(date, days) {
  date.setDate(date.getDate() - days);
  return date.getDate();
}

…但该函数不应更改 date。这是一件重要的事情,因为为我们提供日期的外部代码不希望它发生更改。

为了实现它,让我们克隆日期,如下所示

function getDateAgo(date, days) {
  let dateCopy = new Date(date);

  dateCopy.setDate(date.getDate() - days);
  return dateCopy.getDate();
}

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015)
alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014)
alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014)

在沙盒中打开带有测试的解决方案。

重要性:5

编写函数 getLastDayOfMonth(year, month),它返回该月的最后一天。有时是 30 日、31 日,甚至 2 月的 28/29 日。

参数

  • year – 四位数的年份,例如 2012 年。
  • month – 月份,从 0 到 11。

例如,getLastDayOfMonth(2012, 1) = 29(闰年,2 月)。

打开一个带有测试的沙盒。

让我们使用下个月创建一个日期,但将 0 作为日期

function getLastDayOfMonth(year, month) {
  let date = new Date(year, month + 1, 0);
  return date.getDate();
}

alert( getLastDayOfMonth(2012, 0) ); // 31
alert( getLastDayOfMonth(2012, 1) ); // 29
alert( getLastDayOfMonth(2013, 1) ); // 28

通常,日期从 1 开始,但从技术上讲,我们可以传递任何数字,日期将自动调整自身。因此,当我们传递 0 时,这意味着“本月 1 日前一天”,换句话说:“上个月的最后一天”。

在沙盒中打开带有测试的解决方案。

重要性:5

编写函数 getSecondsToday(),它返回从今天开始的秒数。

例如,如果现在是 上午 10:00,并且没有夏令时转换,那么

getSecondsToday() == 36000 // (3600 * 10)

该函数应在任何一天工作。也就是说,它不应具有“今天”的硬编码值。

要获取秒数,我们可以使用当前日期和时间 00:00:00 生成一个日期,然后从“现在”中减去它。

差值为从一天开始的毫秒数,我们应将其除以 1000 以获取秒数

function getSecondsToday() {
  let now = new Date();

  // create an object using the current day/month/year
  let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());

  let diff = now - today; // ms difference
  return Math.round(diff / 1000); // make seconds
}

alert( getSecondsToday() );

另一种解决方法是获取小时/分钟/秒并将其转换为秒

function getSecondsToday() {
  let d = new Date();
  return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
}

alert( getSecondsToday() );
重要性:5

创建一个函数 getSecondsToTomorrow(),它返回到明天的秒数。

例如,如果现在是 23:00,那么

getSecondsToTomorrow() == 3600

P.S. 该函数应在任何一天工作,“今天”不是硬编码的。

要获取到明天的毫秒数,我们可以从“明天 00:00:00”中减去当前日期。

首先,我们生成“明天”,然后执行

function getSecondsToTomorrow() {
  let now = new Date();

  // tomorrow date
  let tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1);

  let diff = tomorrow - now; // difference in ms
  return Math.round(diff / 1000); // convert to seconds
}

备选解决方案

function getSecondsToTomorrow() {
  let now = new Date();
  let hour = now.getHours();
  let minutes = now.getMinutes();
  let seconds = now.getSeconds();
  let totalSecondsToday = (hour * 60 + minutes) * 60 + seconds;
  let totalSecondsInADay = 86400;

  return totalSecondsInADay - totalSecondsToday;
}

请注意,许多国家/地区实行夏令时 (DST),因此可能存在 23 或 25 小时的日子。我们可能希望分别处理此类日子。

重要性:4

编写一个函数 formatDate(date),它应按如下方式格式化 date

  • 如果自 date 传递的时间少于 1 秒,则为 “现在”
  • 否则,如果自 date 传递的时间少于 1 分钟,则为 “n 秒前”
  • 否则,如果少于一小时,则为 “m 分钟前”
  • 否则,为 “DD.MM.YY HH:mm” 格式的完整日期。即:“day.month.year hours:minutes”,全部采用 2 位数字格式,例如 31.12.16 10:00

例如

alert( formatDate(new Date(new Date - 1)) ); // "right now"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago"

// yesterday's date like 31.12.16 20:00
alert( formatDate(new Date(new Date - 86400 * 1000)) );

打开一个带有测试的沙盒。

要从 date 到现在的时间,让我们减去日期。

function formatDate(date) {
  let diff = new Date() - date; // the difference in milliseconds

  if (diff < 1000) { // less than 1 second
    return 'right now';
  }

  let sec = Math.floor(diff / 1000); // convert diff to seconds

  if (sec < 60) {
    return sec + ' sec. ago';
  }

  let min = Math.floor(diff / 60000); // convert diff to minutes
  if (min < 60) {
    return min + ' min. ago';
  }

  // format the date
  // add leading zeroes to single-digit day/month/hours/minutes
  let d = date;
  d = [
    '0' + d.getDate(),
    '0' + (d.getMonth() + 1),
    '' + d.getFullYear(),
    '0' + d.getHours(),
    '0' + d.getMinutes()
  ].map(component => component.slice(-2)); // take last 2 digits of every component

  // join the components into date
  return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':');
}

alert( formatDate(new Date(new Date - 1)) ); // "right now"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago"

// yesterday's date like 31.12.2016 20:00
alert( formatDate(new Date(new Date - 86400 * 1000)) );

备选解决方案

function formatDate(date) {
  let dayOfMonth = date.getDate();
  let month = date.getMonth() + 1;
  let year = date.getFullYear();
  let hour = date.getHours();
  let minutes = date.getMinutes();
  let diffMs = new Date() - date;
  let diffSec = Math.round(diffMs / 1000);
  let diffMin = diffSec / 60;
  let diffHour = diffMin / 60;

  // formatting
  year = year.toString().slice(-2);
  month = month < 10 ? '0' + month : month;
  dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth;
  hour = hour < 10 ? '0' + hour : hour;
  minutes = minutes < 10 ? '0' + minutes : minutes;

  if (diffSec < 1) {
    return 'right now';
  } else if (diffMin < 1) {
    return `${diffSec} sec. ago`
  } else if (diffHour < 1) {
    return `${diffMin} min. ago`
  } else {
    return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}`
  }
}

在沙盒中打开带有测试的解决方案。

教程地图

评论

评论前请阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行,请将其包装在 <pre> 标记中,对于 10 行以上的内容,请使用沙箱 (plnkrjsbincodepen…)