Автоисправление даты
Автоисправление – это очень полезная особенность объектов Date
. Можно устанавливать компоненты даты вне обычного диапазона значений, а объект сам себя исправит.
Пример:
let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!?
alert(date); // ...1st Feb 2013!
Неправильные компоненты даты автоматически распределяются по остальным.
Предположим, нам требуется увеличить дату «28 февраля 2016» на два дня. В зависимости от того, високосный это год или нет, результатом будет «2 марта» или «1 марта». Нам об этом думать не нужно. Просто прибавляем два дня. Объект 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 ); // выводит правильную дату
Также можно установить нулевые или даже отрицательные значения. Например:
let date = new Date(2016, 0, 2); // 2 Jan 2016
date.setDate(1); // задать первое число месяца
alert( date );
date.setDate(0); // первый день месяца -- это 1, так что выводится последнее число предыдущего месяца
alert( date ); // 31 Dec 2015
Преобразование к числу, разность дат
Если объект 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( `Цикл отработал за ${end - start} миллисекунд` );
Date.now()
Если нужно просто измерить время, объект Date
нам не нужен.
Существует особый метод Date.now()
, возвращающий текущую метку времени.
Семантически он эквивалентен new Date().getTime()
, однако метод не создаёт промежуточный объект Date
. Так что этот способ работает быстрее и не нагружает сборщик мусора.
Данный метод используется из соображений удобства или когда важно быстродействие, например, при разработке игр на JavaScript или других специализированных приложений.
Вероятно, предыдущий пример лучше переписать так:
let start = Date.now(); // количество миллисекунд с 1 января 1970 года
// выполняем некоторые действия
for (let i = 0; i < 100000; i++) {
let doSomething = i * i * i;
}
let end = Date.now(); // заканчиваем отсчёт времени
alert( `Цикл отработал за ${end - start} миллисекунд` ); // вычитаются числа, а не даты
Бенчмаркинг
Будьте внимательны, если хотите точно протестировать производительность функции, которая зависит от процессора.
Например, сравним две функции, вычисляющие разницу между двумя датами: какая сработает быстрее?
Подобные вычисления, замеряющие производительность, также называют «бенчмарками» (benchmark).
// есть date1 и date2, какая функция быстрее вернёт разницу между ними в миллисекундах?
function diffSubtract(date1, date2) {
return date2 - date1;
}
// или
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( 'Время diffSubtract: ' + bench(diffSubtract) + 'мс' );
alert( 'Время diffGetTime: ' + bench(diffGetTime) + 'мс' );
Вот это да! Метод getTime()
работает ощутимо быстрее! Всё потому, что не производится преобразование типов, и интерпретаторам такое намного легче оптимизировать.
Замечательно, это уже что-то. Но до хорошего бенчмарка нам ещё далеко.
Представьте, что при выполнении bench(diffSubtract)
процессор параллельно делал что-то ещё, также потребляющее ресурсы. А к началу выполнения bench(diffGetTime)
он это уже завершил.
Достаточно реалистичный сценарий в современных многопроцессорных операционных системах.
В итоге у первого бенчмарка окажется меньше ресурсов процессора, чем у второго. Это может исказить результаты.
Для получения наиболее достоверных результатов тестирования производительности весь набор бенчмарков нужно запускать по нескольку раз.
Например, так:
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( 'Итоговое время diffSubtract: ' + time1 );
alert( 'Итоговое время diffGetTime: ' + time2 );
Современные интерпретаторы JavaScript начинают применять продвинутые оптимизации только к «горячему коду», выполняющемуся несколько раз (незачем оптимизировать то, что редко выполняется). Так что в примере выше первые запуски не оптимизированы должным образом. Нелишним будет добавить предварительный запуск для «разогрева»:
// добавляем для "разогрева" перед основным циклом
bench(diffSubtract);
bench(diffGetTime);
// а теперь тестируем производительность
for (let i = 0; i < 10; i++) {
time1 += bench(diffSubtract);
time2 += bench(diffGetTime);
}
Будьте осторожны с микробенчмарками
Современные интерпретаторы JavaScript выполняют множество оптимизаций. Они могут повлиять на результаты «искусственных тестов» по сравнению с «нормальным использованием», особенно если мы тестируем что-то очень маленькое, например, работу оператора или встроенной функции. Поэтому если хотите серьёзно понять производительность, пожалуйста, изучите, как работают интерпретаторы JavaScript. И тогда вам, вероятно, уже не понадобятся микробенчмарки.
Отличный набор статей о V8 можно найти на http://mrale.ph.