让我一起探讨一个新的内置对象:日期。该对象存储日期、时间以及提供管理它们的方法。

举个例子,我们可以使用它来存储创建、修改事件的时间,或者用来度量时间开销,再或者用来打印当前时间。

创建

创建一个新的 Date 对象,只需要调用 new Date(),附加下列参数中的其中一个:

new Date()

不带参数 —— 创建一个表示当前日期和时间的 Date 对象:

let now = new Date();
alert( now ); // 显示当前的日期/时间
new Date(milliseconds)

创建一个 Date 对象,参数是从 1970-01-01 00:00:00 UTC+0 开始所经过的毫秒(一秒的千分之一)数。

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

// 增加 24 小时,得到 02.01.1970 UTC+0
let Jan02_1970 = new Date(24 * 3600 * 1000);
alert( Jan02_1970 );

传入的参数是自 1970-01-01 00:00:00 开始计算的毫秒数,被称为时间戳

这是一种日期的轻量级表示方法。我们通常使用时间戳来创建一个日期,比如 new Date(timestamp),以及使用 date.getTime() 来将现有的 Date 对象转化为时间戳(下面将提到)。

new Date(datestring)

如果只有一个参数,并且是字符串,那么该参数会通过 Date.parse 算法解析(下面会提到)。

let date = new Date("2017-01-26");
alert(date); // Thu Jan 26 2017 ...
new Date(year, month, date, hours, minutes, seconds, ms)

创建一个 Date 对象,参数是当地时区的日期组合信息。只有前两个参数是必须的。

注意:

  • year 必须是四位数:2013 是合法的,98 是不合法的。
  • month 计数从 0 (一月) 开始,到 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); // 同样,时分秒等默认为 0

时间度量最小精确到 1 毫秒(千分之一秒):

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 有偏移,那么下面代码会显示不同的小时数:

//  当前日期
let date = new Date();

// 当地时区的小时数
alert( date.getHours() );

// 在 UTC+0 时区的小时数(非夏令时的伦敦时间)
alert( date.getUTCHours() );

在以上给出的方法中,有两个与众不同的,它们没有 UTC 版本:

getTime()

返回日期的时间戳 —— 从 1970-1-1 00:00:00 UTC+0 开始的毫秒数。

getTimezoneOffset()

返回时区偏移数,以分钟为单位:

// 如果你在时区 UTC-1,输出 60
// 如果你在时区 UTC+3,输出 -180
alert( new Date().getTimezoneOffset() );

设置日期信息

以下方法可以设置日期/时间信息:

以上方法除了 setTime() 都有 UTC 版本,比如 setUTCHours()

我们可以看到,有些方法可以一次性设置多个信息,比如 setHours。另外,在这些方法中没有提到的信息将不会被修改。

举个例子:

let today = new Date();

today.setHours(0);
alert(today); // 日期依然是今天,只不过小时数改为 0

today.setHours(0, 0, 0, 0);
alert(today); // 日期依然是今天,时间为 00:00:00。

自动校准

自动校准Date 对象一个非常方便的特性。我们可以设置超范围的数值,它会自动校准。

举个例子:

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

超出范围的日期信息会被自动分配。

假设我们要在日期「2016 年 2 月 28 日」上再加 2 天。结果可能是「3 月 2 日」或者「3 月 1 日」,原因是闰年的存在。但是我们不需要去考虑这些,直接加两天,剩下的 Date 对象会帮我们处理:

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

alert( date ); // 2016 年 3 月 1 日

这个特性经常被用来获取一段时间后的日期信息。举个例子,我们想得到「当前日期 70 秒之后的日期」:

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

alert( date ); // 显示正确的日期信息

我们可以设置 0 甚至 负值。举个例子:

let date = new Date(2016, 0, 2); // 2016 年 1 月 2 日

date.setDate(1); // 设置为当月的第一天
alert( date );

date.setDate(0); // 天数最小可以设置为 1,所以这里设置为上一月的最后一天
alert( date ); // 2015 年 12 月 31 日

日期转化为数字,日期差值

Date 对象转化为数字时,得到的是对应的时间戳,相当于 date.getTime()

let date = new Date();
alert(+date); // 以毫秒为单位的数值,相当于 date.getTime()

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

这个作用可以用来做时间度量:

let start = new Date(); // 起始时间

// 做一些操作
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}

let end = new Date(); // 结束时间

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

Date.now()

如果我们仅仅想要度量时间间隔,我们不需要整个 Date 对象。

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

它相当于 new Date().getTime(),但它不会在中间创建一个 Date 对象。因此它更快,而且不会对垃圾处理造成额外的压力。

这种方法很多时候因为方便而被采用,又或者从性能上考虑,像 JavaScript 中的游戏以及其他的特殊应用。

因此这样做可能会更好:

let start = Date.now(); // 从 1979-01-01 00:00:00 开始至今的时间戳

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

let end = Date.now(); // 操作完成后,得到这一时刻的时间戳

alert( `The loop took ${end - start} ms` ); // 相减的是时间戳,而不是日期

基准

如果我们想要为一个很耗 CPU 性能的函数提供一个可靠的基准,我们应该小心一点。

举个例子:我们想判断两个计算日期差值的函数,那个更快?

// 我们有 date1 和 date2,哪个函数会更快返回两者的时间差?
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;

// 交替运行 bench(upperSlice) 和 bench(upperLoop) 10 次
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 引擎的先进优化策略只对执行很多次的 “hot code” 有效(对于执行很少次数的代码没有必要优化)。因此,以上的例子中,第一部分不会被优化,我们可能需要增加一个升温步骤:

// 主循环中增加「升温」环节
bench(diffSubtract);
bench(diffGetTime);

// 开始度量
for (let i = 0; i < 10; i++) {
  time1 += bench(diffSubtract);
  time2 += bench(diffGetTime);
}
做微度量时请小心

现代的 JavaScript 引擎会做很多优化。相对于「正常情况」,它们可能会改变「人为测试」的结果,特别是我们度量的目标很细微。因此,如果你想好好了解一下性能,请学习 JavaScript 引擎的工作原理。在那之后,你可能再也不需要微度量了。

关于 V8 引擎的大量文章,点击:http://mrale.ph.

对一个字符串使用 Date.parse

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

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

  • YYYY-MM-DD —— 日期:年-月-日。
  • 字符串 "T" 是一个分隔符。
  • HH:mm:ss.sss —— 时间:小时,分钟,秒,毫秒。
  • 可选字符 'Z' 代表时区。单个字符 Z 代表 UTC+0。

简短形式也是可以的,比如 YYYY-MM-DD 或者 YYYY-MM 又或者 YYYY

Date.parse(str) 方法会转化一个特定格式的字符串,返回一个时间戳(自 1970-01-01 00:00:00 起的毫秒数),如果格式不正确,返回 NaN

举个例子:

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

alert(ms); // 1327611110417  (时间戳)

我们可以通过时间戳来立即创建一个 new Date 对象戳:

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

alert(date);

小结

  • 在 JavaScript 中,日期和时间使用 Date 对象来表示。不能只创建日期,或者只创建时间,Date 对象总是两个都创建。
  • 月份从 0 开始计数(对,一月是 0)。
  • 一周的某一天 getDay() 同样从 0 开始计算(0 代表星期天)。
  • 当超出范围的信息被设置时,Date 会做自我校准。这一点对于日/月/小时 的加减很有效。
  • 日期可以相减,得到的是两者的差值,用毫秒表示。因为当转化为数字时,Date 对象变为时间戳。
  • 使用 Date.now() 可以更快地得到当前时间的时间戳。

和其他语言不同,JavaScript 中时间戳是用毫秒表示,而不是秒。

同样,有时候我们会需要更加精准的时间度量。JavaScript 自身并不能度量微秒(一秒的百万分之一),但很多环境会提供。举个例子:浏览器拥有 performance.now() 方法来提供页面加载的微秒数(毫秒的小数点再右移三位):

alert(`Loading started ${performance.now()}ms ago`);
// 得到 "Loading started 34731.26000000001ms ago"
// .26 is 微秒(260 微秒)
// 小数点后超过 3 位是错误,只有前三位是正确的

Node.JS 拥有 microtime 模块以及其他方法。从技术上来说,任何设备和环境都允许获取更精确的数值,不只是 Date 对象。

任务

重要程度: 5

创建一个 Date 对象,日期是:Feb 20, 2012, 3:12am。时区是当地时区。

使用 alert 显示结果。

new Date 构造函数默认使用当地时区。所以唯一需要牢记的是月份从 0 开始计数。

所以二月对应的数值是 1。

let d = new Date(2012, 1, 20, 3, 12);
alert( d );
重要程度: 5

写一个函数 getWeekDay(date) 来显示一个日期的星期数,用简写表示:‘MO’、‘TU’、‘WE’、‘TH’、‘FR’、‘SA’、‘SU’。

举个例子

let date = new Date(2012, 0, 3);  // 3 Jan 2012
alert( getWeekDay(date) );        // 应该输出 "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),返回日期的欧洲式星期数。

let date = new Date(2012, 0, 3);  // 3 Jan 2012
alert( getLocalDay(date) );       // 星期二,应该返回 2

打开带有测试的沙箱。

function getLocalDay(date) {

  let day = date.getDay();

  if (day == 0) { // 0,改为 7
    day = 7;
  }

  return day;
}

alert( getLocalDay(new Date(2012, 0, 3)) ); // 2

使用沙箱的测试功能打开解决方案。

重要程度: 4

写一个函数 getDateAgo(date, days),返回特定日期 date 往前 days 天数后,是当月的哪一天。

举个例子,今天是 20 号,那么 getDateAgo(new Date(), 1) 应该是 19 号,getDateAgo(new Date(), 2) 应该是 18 号。

跨月、年也应该是正确输出:

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)

另:函数不能修改给定的 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,甚至是二月的 28/29。

参数:

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

举个例子,getLastDayOfMonth(2012, 1) = 29

打开带有测试的沙箱。

Let’s create a date using the next month, but pass zero as the day:

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

Normally, dates start from 1, but technically we can pass any number, the date will autoadjust itself. So when we pass 0, then it means “one day before 1st day of the month”, in other words: “the last day of the previous month”.

使用沙箱的测试功能打开解决方案。

重要程度: 5

写一个函数 getSecondsToday(),返回今天已经过去了多少秒?

举个例子:如果现在是10:00 am,并且没有夏令时转移,那么:

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

该函数应该在任意一天都能正确运行。那意味着,不应该有一个「今天」这个参数不能有意外值。

为获取秒数,我们可以创建一个日期,使用今天的日期和 00:00:00 这个时间,然后当前时间减去该时间。

不同之处在于,这样得到的今天之初的日期是毫秒计算,我们应该除以 1000,得到秒数:

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

  // 创建一个对象,使用当前的 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() );

另一种解决方法是得到 hours/minutes/seconds,然后把它们转化为秒数:

function getSecondsToday() {
  let d = new Date();
  return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
};
重要程度: 5

写一个函数 getSecondsToTomorrow(),返回距离明天的秒数。

举个例子,现在是 23:00,那么:

getSecondsToTomorrow() == 3600

另:该函数应该能在任意一天运行

为获取距离明天的毫秒数,我们可以用「第二天 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 秒,输出 "刚刚"
  • 否则,如果少于 1 分钟,输出 "n 秒之前"
  • 否则,如果少于 1 小时,输出 "n 分钟之前"
  • 否则,输出完整日期,用格式"DD.MM.YY HH:mm"。即:"day.month.year hours:minutes",所有的数都用两位数表示,例如: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.2016, 20:00
alert( formatDate(new Date(new Date - 86400 * 1000)) );

打开带有测试的沙箱。

为获取 date 距离当前时间的间隔 —— 我们将两个日期相减。

function formatDate(date) {
  let diff = new Date() - date; // 差值用毫秒表示

  if (diff < 1000) { // 少于一秒
    return 'right now';
  }

  let sec = Math.floor(diff / 1000); // 将间隔转化为秒

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

  let min = Math.floor(diff / 60000); // 将间隔转化为分钟
  if (min < 60) {
    return min + ' min. ago';
  }

  // 格式化日期
  // 在单个数值之前加 0 日/月/小时/分钟
  let d = date;
  d = [
    '0' + d.getDate(),
    '0' + (d.getMonth() + 1),
    '' + d.getFullYear(),
    '0' + d.getHours(),
    '0' + d.getMinutes()
  ].map(component => component.slice(-2)); // 得到每个组件的后两位

  // 将时间信息和日期组合在一起
 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"

// 昨天的日期如: 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;

  // 格式化
  year = year.toString().slice(-2);
  month = month < 10 ? '0' + month : month;
  dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth;

  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}`
  }
}

使用沙箱的测试功能打开解决方案。

教程路线图

评论

在评论之前先阅读本内容…
  • 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
  • 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
  • 如果你无法理解文章中的内容 — 请详细说明。