При работе с Date в JavaScript были некоторые ошибки, которые нам пришлось преодолеть на собственном горьком опыте. Я надеюсь взорвать вам мозг контролируемым образом сейчас, вместо того, чтобы потом взорвать его из-за неприятного бага.

Инициализация Date строкой неоднозначна.

Начнем с примера. Откройте консоль в Chrome и запустите это:

> new Date('2018-3-14')
⋖ Wed Mar 14 2018 00:00:00 GMT+0545 (Nepal Time)

Хорошо, это сработало. Теперь сделайте то же самое в Safari (или просто поверьте мне, если у вас сейчас нет доступа к Safari):

> new Date('2018-3-14')
⋖ Invalid Date

Чего ждать!?

В спецификации ECMAScript говорится, что вызов конструктора Date с одним строковым аргументом создаст новую Date с той же реализацией, что и Date.prototype.parse.

Далее говорится, что Date.prototype.parse будет зависеть от реализации, если строка не является чем-то, что может быть сгенерировано Date.prototype.toString или Date.prototype.toUTCString.

По сути, если строка, которую вы пытаетесь проанализировать, не соответствует формату, заданному Date.toString() или Date.toUTCString(), вы облажались.

Chrome просто хочет быть экстраординарным и изо всех сил старается поддерживать больше форматов; но на самом деле это дает разработчикам ложное ощущение безопасности, что их код работает. Это дало нам достаточное количество ситуаций типа «но… это работает на моей машине».

Хорошо, тогда какие форматы new Date() правильно поддерживает?

Это ... также зависит от того, в каком браузере вы работаете. Вот цитата из спецификаций:

Содержимое строки зависит от реализации, но предназначено для представления даты в удобной, удобочитаемой форме ...

К счастью, в данном случае все согласны с использованием формата даты ISO 8601. Это довольно просто и вы, вероятно, уже пользуетесь им:

2018–06–17 // Notice it's 06 not 6
2018–06–17T07:11:54+00:00
2018–06–17T07:11:54Z
20180617T071154Z

Урок 1. Всегда используйте формат даты и времени ISO 8601, везде и всегда. Шутки в сторону.

Обновление
Начиная с ES5, были обновлены спецификации, которые определяют ISO 8601 в самих спецификациях JavaScript, и это уже не просто консенсус.

Проблема с часовым поясом в целом.

Внутренний объект Date в JavaScript - это просто число, хранящее количество миллисекунд с 1 января 1970 года по всемирному координированному времени.

JavaScript Date имеет очень элементарное представление о часовых поясах и переходе на летнее время. Он вроде как знает, что такое смещение часового пояса компьютера, на котором он работает, и применяется ли DST прямо сейчас (в обоих случаях он полагается на браузер, который полагается на ОС).

У него нет возможности выяснить, какое время находится в разных часовых поясах или к какому часовому поясу привязан конкретный объект Date. Фактически, нет способа привязать объект Date к определенному часовому поясу, все операции с объектом Date основаны на локальном часовом поясе системы, в которой он работает.

Все, что делает объект Date, зависит от того внутреннего числа миллисекунд, которое он имеет в каждом объекте Date. Таким образом, единственное реальное влияние часовых поясов - это только тогда, когда мы инициализируем этот внутренний номер.

Например, когда вы говорите new Date('2018-04-14'), что должен понимать объект даты? Это может быть 1520985600000, если дата находится в формате UTC или 1520964900000, если дата находится в +05:45 (Nepal Time).

Знание того, когда JavaScript понимает, что важно для выяснения проблемы с часовым поясом.

Вот краткий обзор возможностей:

Дата инициализируется строкой даты и времени ISO 8601.

> const d = new Date('2018-04-14');
> d.toUTCString();
⋖ "Sat, 14 Apr 2018 00:00:00 GMT"
> d.toString();
⋖ "Sat Apr 14 2018 05:45:00 GMT+0545"

Это главный виновник большинства проблем, связанных с датой и временем. Представьте, что вы берете этот объект Date и делаете на нем getDate(). Что будет в результате? 14, верно?

> d.getDate();
⋖ 14

Вот в чем загвоздка: посмотрите на временную часть в выводе d.toString() выше. Поскольку объект Date работает только с часовым поясом локальной системы, все, что он делает с объектом Date, основано на местном часовом поясе.

Что, если бы мы запустили тот же код на компьютере в Нью-Йорке?

> const d = new Date('2018-04-14');
> d.toUTCString();
⋖ "Sat, 14 Apr 2018 00:00:00 GMT"
> d.toString();
⋖ "Fri Apr 13 2018 14:15:00 GMT-0400"

И какая это дата?

> d.getDate();
⋖ 13

Если подумать, это очевидно. 2018-04-14 00:00 в Лондоне, 2018-04-14 05:14 в Непале и 2018-04-13 14:15 в Нью-Йорке.

Как выяснилось, 2018-04-14 было короткой рукой для 2018-04-14T00:00:00Z. Видите Z в конце? Это означает, что указанная дата находится в формате UTC.

Результаты будут другими, если мы избавимся от Z.

> const d = new Date('2018-04-14T00:00:00+05:45');
> d.toUTCString();
⋖ "Fri, 13 Apr 2018 18:15:00 GMT"

Что верно, полночь 14 апреля в Непале - 18:15 13 апреля в Лондоне. Тем не менее, d.getDate() даст 14 в Непале, но 13 где-нибудь к западу от Непала.

Урок 2: Дата, инициализированная из ISO 8601, строки даты и времени анализируются в соответствии с информацией о часовом поясе в строке, но инициализируются в соответствии с местным временем.

Дата не инициализируется строками.

new Date(2018, 3, 14, 0, 0, 0, 0);

Угадай, какая это дата. 14 марта 2018 г.? Неправильный. Это 14 апреля 2018 года. Видите ли, в мире JavaScript месяцы начинаются с 0. Но дни по-прежнему начинаются с 1. Не спрашивайте меня, почему.

Но приятно то, что 14 апреля 2018 года на каждом компьютере в любой части мира.

Когда вы инициализируете объект Date напрямую с аргументами, всегда считается, что он находится в местном часовом поясе.

Это ваше решение для таких вещей, как дни рождения, которые представляют собой просто дату, и неважно, в каком часовом поясе она инициализирована. Для большинства других вещей, если имеет значение, когда и где именно что-то произошло, может быть лучше придерживаться ISO 8601.

Но что, если у вас есть дата-время, которую нужно инициализировать в формате UTC? Превратить его в строку ISO 8601? Может быть… или просто использовать Date.UTC.

> // These two are equivalent:
> const a = new Date('2018-04-16');
> const b = new Date(Date.UTC(2018, 3, 16));
> a.toString() === b.toString();
⋖ true

Урок 3: Если вы параноик или просто работаете с местным временем, всегда инициализируйте Date напрямую с помощью аргументов.

Строки, не соответствующие ISO 8601.

Как упоминалось ранее, строки, которые не соответствуют формату ISO 8601, неоднозначно анализируются между браузерами. Но стоит обсудить наиболее распространенные реализации.

Chrome поддерживает множество форматов (стоит отметить, что Node использует тот же движок V8, что и Chrome, поэтому результаты такие же):

new Date('April 13') --> April 13 2001 Local timezone
new Date('5/13/2012') --> May 13 2012 Local timezone
new Date('15/12/2009') --> Invalid Date (Finally!)

В Firefox:

new Date('April 13') --> Invalid Date
new Date('5/13/2012') --> May 13 2012 Local timezone
new Date('15/12/2009') --> Invalid Date

Firefox кажется немного более строгим, но Safari - безусловно, самым строгим.

Здесь следует отметить, что все они находятся в локальном часовом поясе, как если бы они были инициализированы непосредственно из аргументов.

Но есть и исключение. Учти это:

new Date('2018-04-16T00:00:00')

Это ISO 8601? Почти, но нет. В этой строке нет часового пояса. Так что это тоже попадает в неоднозначную группу.

В Chrome:

> new Date('2018-04-16T00:00:00')
⋖ Mon Apr 16 2018 00:00:00 GMT+0545 (Nepal Time)

Разбирается по местному времени.

В Safari:

> new Date('2018-04-16T00:00:00')
⋖ Mon Apr 16 2018 05:45:00 GMT+0545 (+0545)

Разбирается как UTC.

Это может вызвать путаницу и головную боль, если вы тестируете только в Chrome.

Повторяю, используйте только строки даты в формате ISO 8601. Всегда.

NVM, я просто воспользуюсь моментом.

Во-первых, момент - не панацея против каждой проблемы с датой JavaScript. Во-вторых, многие предостережения во внутреннем объекте Date по-прежнему относятся к моменту.

Например, оба из них дадут вам Invalid Date в Safari, но будут нормально работать в Chrome:

> new Date('2018-3-14')
⋖ Invalid Date
> moment('2018-3-14')
⋖ Invalid Date

Кроме того, я видел проекты, у которых на данный момент размер пакета больше половины их размера. Возможно, вначале это не то, о чем вы заботитесь, но оно наверняка укусит вас в будущем, и к тому времени может быть уже слишком поздно возвращаться.

Ничего не имею против моментов, интенсивно им пользуюсь - но на бэкэнде без ограничений по размеру; Я до сих пор не нашел убедительного варианта использования его во внешнем интерфейсе. Может быть, DateFNS подойдет для вашего варианта использования?

Заключить.

Ошибки, связанные с неверными предположениями об объекте Date, являются обычным явлением, и каждый в конечном итоге столкнется с этим. Лучшее понимание того, как все работает, а также установление и применение лучших практик - это, возможно, единственный способ обойти это. У нас была своя доля прочесывания кода и поиска ошибок, чтобы найти дефектный объект Date под всем этим.