Javascript: прощай, Date, здравствуй, Temporal
- четверг, 15 января 2026 г. в 00:00:07

Время выставляет нас всех дураками, и JavaScript в этом не исключение. Честно говоря, меня это особо не задевало: по большей мере меня радуют маленькие странности JavaScript.
Мне нравится, когда можно увидеть обратную сторону; какой бы формальной и железобетонной ни казалась спецификация ES-262, мы всё равно замечаем (если знать, куда смотреть) в ней все хорошие и плохие решения, принятые сотнями людей, разрабатывавших язык. У JavaScript есть характер. Да, он не всегда делает всё в точности так, как можно ожидать, но на мой взгляд, JavaScript обладает настоящим очарованием, которое можно оценить, если глубоко его изучить.
Впрочем, существует одна часть языка, которая мне кажется совершенно нелогичной.
// Числовое представление месяцев начинается с нуля,а годов и дней нет:
console.log( new Date(2026, 1, 1) );
// Результат: Date Sun Feb 01 2026 00:00:00 GMT-0500 (Eastern Standard Time)Это конструктор Date.
// Подразумевается, что числовая строка от 32 до 49 относится к 2000-м:
console.log( new Date( "49" ) );
// Результат: Date Fri Jan 01 2049 00:00:00 GMT-0500 (Eastern Standard Time)
// Числовая строка от 33 до 99 относится к 1900-м:
console.log( new Date( "99" ) );
// Результат: Date Fri Jan 01 1999 00:00:00 GMT-0500 (Eastern Standard Time)
// ...Но строки от 100 и выше начинаются с нулевого года:
console.log( new Date( "100" ) );
// Результат: Date Fri Jan 01 0100 00:00:00 GMT-0456 (Eastern Standard Time)Мне ужасно не нравится Date.
// Строковая дата работает именно так, как и следует ожидать:
console.log( new Date( "2026/1/2" ) );
// Результат: Date Fri Jan 02 2026 00:00:00 GMT-0500 (Eastern Standard Time)
// Ноль в начале месяца? Не проблема: числа остаются числами.
console.log( new Date( "2026/02/2" ) );
// Результат: Date Mon Feb 02 2026 00:00:00 GMT-0500 (Eastern Standard Time)
// Форматирование немного отличается? Всё в порядке!
console.log( new Date( "2026-02-2" ) );
// Результат: Date Mon Feb 02 2026 00:00:00 GMT-0500 (Eastern Standard Time)
// Ноль в начале дня? Конечно, почему это не должно работать?
console.log( new Date('2026/01/02') );
// Результат: Date Fri Jan 02 2026 00:00:00 GMT-0500 (Eastern Standard Time)
// Разумеется, если только не разделить год, месяц и день дефисами.
// Тогда _день_ оказывается не тем.
console.log( new Date('2026-01-02') );
// Результат: Date Thu Jan 01 2026 19:00:00 GMT-0500 (Eastern Standard Time)Date — это отстой. Его бессовестно, но с ошибками содрали из Java, поэтому всё реализовано неправильно, вплоть до названия: Date представляет не дату, а время. Внутри него даты хранятся в виде числовых значений, называемых значениями времени (time value): это временные метки Unix, разделённые на 1000 миллисекунд; да, время Unix необязательно подразумевает дату, но тем не менее, Date представляет время, из которого можно извлечь дату. Отвратительно.
// Временная метка Unix для Monday, December 4, 1995 12:00:00 AM GMT-05 (the day JavaScript was announced):
const timestamp = 818053200;
console.log( new Date( timestamp * 1000 ) );
// Результат: Date Mon Dec 04 1995 00:00:00 GMT-0500 (Eastern Standard Time)Слова «дата» и «время» имеют конкретный смысл, но ладно, как знаешь, JavaScript.
Date в Java стали считать устаревшим с 1997 года, всего два года спустя после того, как ничего не подозревающий мир подвергся атаке Date в JavaScript. Как мы уже видели, при парсинге дат он крайне не согласован. В нём нет понятия часовых поясов, за исключением локального GMT, что неидеально для общемирового использования; кроме того Date учитывает только григорианский календарь. Он совершенно не понимает концепцию летнего времени. Из-за всех этих недостатков крайне часто пользуются сторонними библиотеками, и некоторые из них совершенно огромны; это реальным и ощутимым образом снижает производительность веба, вредя ему.
Но моя основная претензия к Date не в этом, она концептуальна: использование этого объекта подразумевает отклонение от фундаментальной природы самого времени.
Все значения примитивов JavaScript неизменяемы (immutable), то есть сами значения поменять нельзя. Числовое значение 3 никогда не представляет ничего, кроме концепции «три», а true не может значить ничего, кроме истины. Это значения с конкретным смыслом из реального мира. Мы знаем, что такое «три». Тройка не может стать «не-тройкой». Такие неизменяемые типы данных хранятся по значению, то есть переменная, представляющая числовое значение 3, по сути, «содержит» числовое значение 3., а значит, и ведёт себя соответственно.
Когда переменной присваивается неизменяемое значение, движок JavaScript создаёт копию этого значения и сохраняет копию в память:
const theNumber = 3;
console.log( theNumber );
// Результат: 3Это вполне соответствует общепринятой ментальной модели «переменной»: theNumber «содержит» 3.
Когда мы инициализируем theOtherNumber со значением, привязанным к with theNumber, ментальная модель остаётся такой же: 3 снова создаётся и сохраняется в память. Теперь theOtherNumber можно воспринимать как содержащую собственную отдельную 3.
const theNumber = 3;
const theOtherNumber = theNumber;
console.log( theOtherNumber );
// Результат: 3;Разумеется, значение theNumber не меняется, когда мы изменяем значение, связанное с theOtherNumber — мы снова работаем с двумя отдельными экземплярами 3.
const theNumber = 3;
let theOtherNumber = theNumber;
theOtherNumber = 5;
console.log( theOtherNumber );
// Результат: 5;
console.log( theNumber );
// Результат: 3При изменении значения, привязанного к theOtherNumber, мы изменяем не 3, а создаём новое неизменяемое число и привязываем его. В этом и причина ошибки, когда мы пытаемся работать с переменной, объявленной с использованием const:
const theNumber = 3;
theNumber = 5;
// Результат: Uncaught TypeError: invalid assignment to const 'theNumber'Мы не можем менять привязку const и совершенно точно не можем менять смысл 3.
Типы данных, которые могут изменяться после создания, называются изменяемыми (mutable), то есть само значение данных может меняться. Значения объектов, то есть любые непримитивные значения наподобие массива, map или множества, изменяемы.
Переменные (и свойства объектов, параметры функций, элементы массива, множества или map) не могут «содержать» объект в том смысле, в котором мы воспринимаем theNumber в примере выше «содержащим» 3. Переменная может содержать или примитивное значение, или ссылочное значение; последнее — это указатель на адрес хранения объекта в памяти. Когда мы присваиваем объект переменной, вместо создания копии этого объекта создаётся идентификатор, представляющий ссылку на адрес хранения объекта в памяти. Именно поэтому объект, привязанный к переменной, объявленной с const, всё равно может изменяться: ссылочное значение менять нельзя, но можно менять значения объекта:
const theObject = {
theValue : 3
};
theObject.theValue++;
console.log( theObject.theValue );
// Результат: 4Поменять привязку const по-прежнему нельзя, но можно изменить объект, на который ссылается привязка.
Когда ссылочное значение присваивается от одной переменной другой, движок JavaScript создаёт копию этого ссылочного значения, а не самого значения объекта. Оба идентификатора указывают на один и тот же объект в памяти — любые изменения, выполняемые с этим объектом посредством одной ссылки, будут отражены остальными, потому что они ссылаются на одно и то же:
const theObject = {
theValue : 3
};
const theOtherObj = theObject;
theOtherObj.theValue++;
console.log( theOtherObj.theValue );
// Результат: 4
console.log( theObject.theValue );
// Результат: 4И вот это бесит меня в обработке дат на JavaScript. Несмотря на то, что они представляют значения «точки в календаре», значения дат в JavaScript изменяемы: Date — это конструктор, и вызов конструктора с new обязательно приводит к созданию объекта, а все объекты по природе своей изменяемы:
const theDate = new Date();
console.log( typeof theDate );
// Результат: objectХотя «January 1st, 2026» — это столь же неизменяемая концепция реального мира, как «три» или «истина», единственный способ описания этой даты — изменяемая структура данных.
Кроме того, это означает, что любая переменная, инициализированная экземпляром конструктора Date, содержит ссылочное значение, указывающее на значение данных в памяти, которое можно изменять при помощи любой ссылки на это значение:
const theDate = new Date();
console.log( theDate.toDateString() );
// Результат: Tue Dec 30 2025
theDate.setMonth( 10 );
console.log( theDate.toDateString() );
// Результат: Sun Nov 30 2025Кстати, стоит снова напомнить, что месяц 10 — это ноябрь.
Итак, несмотря на то, что даты реального мира имеют жёстко заданный смысл, процесс взаимодействия с экземпляром Date , представляющим значение из реального мира, может подразумевать изменение этого экземпляра не всегда ожидаемым для нас образом:
const today = new Date();
const addDay = theDate => {
theDate.setDate( theDate.getDate() + 1 );
return theDate;
};
console.log(`Today is ${ today.toLocaleDateString() }, tomorrow is ${ addDay( today ).toLocaleDateString() }.`);
// Результат: Today is 12/31/2025. Tomorrow is 1/1/2026.Вроде бы пока всё нормально? Сегодня — это сегодня, завтра — завтра; в мире всё правильно. Вполне можно добавить это в кодовую базу и двигаться дальше... Только если мы не изменим немного порядок вывода.
const today = new Date();
const addDay = theDate => {
theDate.setDate( theDate.getDate() + 1 );
return theDate;
};
console.log(`Tomorrow will be ${ addDay( today ).toLocaleDateString() }. Today is ${ today.toLocaleDateString() }.`);
// Результат: Tomorrow will be 1/1/2026. Today is 1/1/2026.Видите, что происходит? Переменная today представляет собой ссылку на объект, созданный при помощи new Date(). Когда мы передаём today в качестве аргумента функции addDay, параметр theDate начинает представлять копию ссылочного значения — не копию значения, а вторую ссылку на объект, представляющий сегодняшнюю дату. Если мы изменим это значение, чтобы определить дату следующего дня, то выполним действие с изменяемым объектом в памяти, а не неизменяемой копией, сегодня превращается в завтра, взмах крыла бабочки на одном конце света приводит к к цунами на другом.
Наверно, вы уже поняли, что я пишу этот пост не для того, чтобы чествовать Date, но, вероятно, не ожидаете, что я хочу похоронить его. Всё именно так: дни Date уже сочтены, он будет считаться «устаревшим» в веб-смысле этого понятия: останется с нами навсегда, но по возможности его больше не стоит использовать. Скоро у нас будет объект, который полностью заменит Date: Temporal.
Внимательные читатели могли заметить, что я сказал «объект», который заменит Date, а не «конструктор». Temporal — это не конструктор, и консоль разработчика в браузере скажет вам то же самое, если вы попытаетесь вызвать его, как конструктор:
const today = new Temporal();
// Uncaught TypeError: Temporal is not a constructorНа мой взгляд, Temporal — это намного более подходящее название для того, что хранит время.
Temporal — это объект пространства имён, то есть обычный объект, состоящий из статических свойств и методов, наподобие объекта Math:
console.log( Temporal );
/* Результат (в развёрнутом виде):
Temporal { … }
Duration: function Duration()
Instant: function Instant()
Now: Temporal.Now { … }
PlainDate: function PlainDate()
PlainDateTime: function PlainDateTime()
PlainMonthDay: function PlainMonthDay()
PlainTime: function PlainTime()
PlainYearMonth: function PlainYearMonth()
ZonedDateTime: function ZonedDateTime()
Symbol(Symbol.toStringTag): "Temporal"
*/В отличие от Date, он кажется мне сразу же понятным. Классы и объекты пространства имён, содержащиеся в Temporal, позволяют вычислять промежутки между двумя точками времени, описывать момент времени с указанием и без указания часового пояса и получать доступ к текущему моменту при помощи свойства Now. Temporal.Now ссылается на объект пространства имён, содержащий собственные свойства и методы:
console.log( Temporal.Now );
/* Результат (в развёрнутом виде):
Temporal.Now { … }
instant: function instant()
plainDateISO: function plainDateISO()
plainDateTimeISO: function plainDateTimeISO()
plainTimeISO: function plainTimeISO()
timeZoneId: function timeZoneId()
zonedDateTimeISO: function zonedDateTimeISO()
Symbol(Symbol.toStringTag): "Temporal.Now"
<prototype>: Object { … }
*/Temporal позволяет нам логично получать текстовую текущую дату в стиле старого Date: свойство Now содержит метод plainDateISO(). Так как мы не указали ничего, связанного с часовым поясом (а благодаря Temporal это теперь можно делать!), этот ��етод возвращает дату в текущем часовом поясе (в моём случае EST):
console.log( Temporal.Now.plainDateISO() );
/* Результат (в развёрнутом виде):
Temporal.PlainDate 2025-12-31
<prototype>: Object { … }
*/Обратили внимание, что результаты plainDateISO уже отформатированы в значение из одной даты? Запомним это, в дальнейшем мы к этому вернёмся.
... Постойте-ка, это выглядит знакомо:
const nowTemporal = Temporal.Now.plainDateISO();
const nowDate = new Date();
console.log( nowTemporal );
/* Результат (в развёрнутом виде):
Temporal.PlainDate 2025-12-31
<prototype>: Object { … }
*/
console.log( nowDate );
/* Результат (в развёрнутом виде):
Date Tue Dec 31 2025 11:05:52 GMT-0500 (Eastern Standard Time)
<prototype>: Date.prototype { … }
*/Не может быть...
const rightNow = Temporal.Now.instant();
console.log( typeof rightNow );
// ОбъектДа, мы по-прежнему работаем с изменяемым объектом, представляющим текущую дату. На первый взгляд кажется, что это вообще не устраняет основную мою претензию к Date.
На самом деле, здесь мы зависим от самой природы языка: даты описывают сложные значения реального мира, для сложных данных необходимы сложные структуры данных, а для JavaScript это означает использование объектов. Разница в том, как мы взаимодействуем с этими объектами Temporal: магия происходит в цепочке прототипов:
const nowTemporal = Temporal.Now.plainDateISO();
console.log( nowTemporal.__proto__ );
/* Результат (в развёрнутом виде):
Object { … }
add: function add()
calendarId: >>
constructor: function PlainDate()
day: >>
dayOfWeek: >>
dayOfYear: >>
daysInMonth: >>
daysInWeek: >>
daysInYear: >>
equals: function equals()
era: >>
eraYear: >>
inLeapYear: >>
month: >>
monthCode: >>
monthsInYear: >>
since: function since()
subtract: function subtract()
toJSON: function toJSON()
toLocaleString: function toLocaleString()
toPlainDateTime: function toPlainDateTime()
toPlainMonthDay: function toPlainMonthDay()
toPlainYearMonth: function toPlainYearMonth()
toString: function toString()
toZonedDateTime: function toZonedDateTime()
until: function until()
valueOf: function valueOf()
weekOfYear: >>
with: function with()
withCalendar: function withCalendar()
year: >>
yearOfWeek: >>
Symbol(Symbol.toStringTag): "Temporal.PlainDate"
<get calendarId()>: function calendarId()
<get day()>: function day()
<get dayOfWeek()>: function dayOfWeek()
<get dayOfYear()>: function dayOfYear()
<get daysInMonth()>: function daysInMonth()
<get daysInWeek()>: function daysInWeek()
<get daysInYear()>: function daysInYear()
<get era()>: function era()
<get eraYear()>: function eraYear()
<get inLeapYear()>: function inLeapYear()
*/Можно сразу заметить, что здесь есть множество методов и свойств для доступа, форматирования и манипуляций с подробностями объекта Temporal, с которым мы работаем. В этом нет ничего удивительного — освоить всё это будет чуть сложнее, но для этого вполне достаточно изучения информации на MDN. Большое отличие от Date заключается в том, как всё это реализовано на фундаментальном уровне:
const nowTemporal = Temporal.Now.plainDateISO();
// Текущая локальная дата:
console.log( nowTemporal );
/* Результат (в развёрнутом виде):
Temporal.PlainDate 2025-12-30
<prototype>: Object { … }
*/
// Текущий локальный год:
console.log( nowTemporal.year );
// Результат: 2025
// Текущие локальные дата и время:
console.log( nowTemporal.toPlainDateTime() );
/* Результат (в развёрнутом виде):
Temporal.PlainDateTime 2025-12-30T00:00:00
<prototype>: Object { … }
*/
// Указываем, что эта дата описывает часовой пояс Европы/Лондона:
console.log( nowTemporal.toZonedDateTime( "Europe/London" ) );
/* Результат (в развёрнутом виде):
Temporal.ZonedDateTime 2025-12-30T00:00:00+00:00[Europe/London]
<prototype>: Object { ��� }
*/
// Прибавляем день к этой дате:
console.log( nowTemporal.add({ days: 1 }) );
/*
Temporal.PlainDate 2025-12-31
<prototype>: Object { … }
*/
// Прибавляем к этой дате один месяц и один день, а затем вычитаем два года:
console.log( nowTemporal.add({ months: 1, days: 1 }).subtract({ years: 2 }) );
/*
Temporal.PlainDate 2024-01-31
<prototype>: Object { … }
*/
console.log( nowTemporal );
/* Результат (в развёрнутом виде):
Temporal.PlainDate 2025-12-30
<prototype>: Object { … }
*/Вы обратили внимание, что ни для одного из этих преобразований нам не понадобилось вручную создавать новые объекты, а значение объекта, на который ссылается nowTemporal, осталось неизменным? В отличие от Date, методы, используемые для взаимодействия с объектом Temporal, приводят к созданию новых объектов Temporal, а не требуют от нас использовать их в контексте нового экземпляра или изменять экземпляр, с которым мы работаем. Благодаря этому в nowTemporal.add({ months: 1, days: 1 }).subtract({ years: 2 }) мы можем объединять в цепочки методы add и subtract.
Да, мы продолжаем работать с объектами, а значит, с изменяемыми структурами данных, описывающими значения реального мира:
const nowTemporal = Temporal.Now.plainDateISO();
nowTemporal.someProperty = true;
console.log( nowTemporal );
/* Результат (в развёрнутом виде):
Temporal.PlainDate 2026-01-05
someProperty: true
<prototype>: Object { … }…Но значение, представленное этим объектом Temporal, не предназначено для изменений при обычном способе взаимодействия с ним — несмотря на то, что объект по-прежнему остаётся изменяемым, мы не вынуждены использовать его в том смысле, который подразумевается с точки зрения реальных дат и времени. С этим я вполне могу смириться.
Давайте же вернёмся к бесячему скрипту «сегодня X, завтра Y», который мы выше написали с использованием Date. Для начала исправим его, чтобы он точно работал с двумя отдельными экземплярами Date, а не изменял экземпляр, представляющий сегодняшнюю дату:
const today = new Date();
const addDay = theDate => {
const tomorrow = new Date();
tomorrow.setDate( theDate.getDate() + 1 );
return tomorrow;
};
console.log(`Tomorrow will be ${ addDay( today ).toLocaleDateString() }. Today is ${ today.toLocaleDateString() }.`);
// Результат: Tomorrow will be 1/1/2026. Today is 12/31/2025.Фу, какой ужас.
Ну ладно, допустим. Он выполняет свою задачу точно так же, как в те времена, когда Date впервые добрался до веба. Мы не сможем случайно изменить значение today , потому что создаём в функции addDay новый экземпляр Date — код получается длинным, но он работает. Мы прибавляем к нему 1, и это, как мы вроде бы знаем, добавляет один день. Далее в шаблонном литерале нам нужно убедить JavaScript выдать дату в виде строки в формате, не включающем в себя текущее время. Это работает, но реализуется слишком длинно.
Давайте теперь перепишем этот код с использованием Temporal:
const today = Temporal.Now.plainDateISO();
console.log(`Tomorrow will be ${ today.add({ days: 1 }) }. Today is ${ today }.`);
// Результат: Tomorrow will be 2026-01-01. Today is 2025-12-31.А вот это уже дело.
Гораздо лучше. Лаконичнее, логичнее и меньше возможностей совершить ошибку. Нам нужна текущая дата без времени, и объект, создаваемый при вызове plainDateISO (а также все новые объекты Temporal, создаваемые из него), сохранит это форматирование без принудительного преобразования в строку. С форматированием разобрались.
Мы хотим вывести значение, описывающее текущую дату плюс один день, и нам нужно сделать это так, чтобы мы безошибочно приказали «прибавь к этому один день» без гаданий с парсингом: оба пункта тоже реализованы.
Ещё важнее то, что нам не приходится рисковать, что исходный объекта today случайно изменится, потому что результатом вызова метода add всегда будет новый объект Temporal.
Temporal станет огромным шагом вперёд относительно Date; я говорю «станет», потому что он ещё не совсем готов к полнофункциональному применению. Драфт спецификации предлагаемого объекта Temporal добрался до третьего этапа процесса стандартизации, то есть теперь он официально «рекомендован к реализации», но пока не стал частью стандарта, лежащего в основе текущей разработки JavaScript. Однако он достаточно близок к этому для того, чтобы браузеры начали реализовывать его поддержку. Это означает, что результаты первых экспериментов могут использоваться для совершенствования спецификации, поэтому пока ничего ещё жёстко не стандартизировано. В конце концов, веб-стандарты — это итеративный процесс.
Теперь на сцену выходим мы с вами. Temporal уже появился в последних версиях Chrome и Firefox (другие браузеры вскоре последуют за ними), поэтому настала пора самостоятельной работы. Пусть мы никак не могли повлиять на внедрение Date, но можем экспериментировать с Temporal, пока не создана окончательная его реализация.
Скоро в JavaScript появится логичный и современный способ работы с датами, а мы, наконец, сможем забросить Date в ящик, где хранятся ключи непонятно от чего, полуразряженные батарейки AA и прочий мусор. Он останется неотъемлемой частью веб-платформы, но перестанет быть первым, последним и единственным способом обработки дат. Нам остаётся только подождать... Погодите, сейчас посчитаю, сколько мы уже ждём:
const today = Temporal.Now.plainDateISO();
const jsShipped = Temporal.PlainDate.from( "1995-12-04" );
const sinceDate = today.since( jsShipped, { largestUnit: 'year' });
console.log( `${ sinceDate.years } years, ${ sinceDate.months } months, and ${ sinceDate.days } days.` );
// Результат: 30 years, 0 months, and 27 days.Да, лучше всего было бы избавиться от Date ещё в 1995 году, но следующий наилучший момент — это Temporal.Now, правда?