ES6: улучшения объекта Number изнутри
Перевод статьи ES6 Number Improvements in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.
Привет! Рад, что вы подоспели вовремя для ES6 — «Снова в школу» — изнутри. Не слышали об этом? Тогда загляните в краткую историю инструментария ES6. Затем изучите деструктирование, литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры, улучшения в литералах объекта, новый «сахарок» — классы поверх прототипов, let, const и «Временную мёртвую зону», а также итераторы, генераторы, символы, объекты Map, WeakMaps, Sets и WeakSets, прокси, ловушки прокси, ещё о ловушках и отражение. Ну а сегодня поговорим об улучшениях объекта Number
.
Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощью интерактивной оболочки REPL, либо командной строки babel-node и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6. Ещё одна довольно полезная альтернатива, это использовать онлайновый REPL для Babel — он показывает скомпилированный ES5-код справа от ES6-кода, чтобы быстро сравнить.
Пока мы не начали, позвольте беззастенчиво попросить вашей поддержки, если вы наслаждаетесь моей серией «ES6 изнутри». Ваши взносы пойдут на хостинг сайту, мне на еду, на то, чтоб я не отставал от графика, и на то, чтобы Pony Foo оставался поистине источником джаваскриптовых вкусняшек.
Спасибо, что выслушали, а теперь перейдём к улучшениям объекта Number! На самом деле, эти изменения не зависят от того, что мы уже рассмотрели — хотя я бы настоятельно рекомендовал ознакомиться со статьями из этой серии. Пришло время для объекта Number
.
Улучшения объекта Number
в ES6
Для объекта числа (Number
) ES6 приготовил изрядное число новинок — как вам каламбур? Вначале предлагаю посмотреть список функций, о которых мы поговорим. Сегодня мы рассмотрим все следующие изменения в объекте Number
.
- Двоичные и восьмеричные литералы — используют префиксы
0b
и0o
Number.isNaN
Number.isFinite
Number.parseInt
Number.parseFloat
Number.isInteger
Number.EPSILON
Number.MAX_SAFE_INTEGER
Number.MIN_SAFE_INTEGER
Number.isSafeInteger
Двоичные и восьмеричные литералы
До ES6 двоичные представления целых чисел проще всего было бы передать в parseInt
с основанием 2
.
parseInt('101', 2) // <- 5
В ES6 для представления двоичных целочисленных литералов также подойдёт префикс 0b
. Можно и 0B
, но я советую придерживаться нижнего регистра.
console.log(0b001) // <- 1 console.log(0b010) // <- 2 console.log(0b011) // <- 3 console.log(0b100) // <- 4
То же касается и восьмеричных литералов. В ES3 parseInt
интерпретировал строки цифр, начинающиеся с 0
, в качестве восьмеричного значения. Поэтому, если не указать основание 10
, сразу же начинались странности — и вскоре все взяли за правило его указывать.
parseInt('01') // <- 1 parseInt('08') // <- 0 parseInt('8') // <- 8
А вот ES5 уже избавился от восьмеричной интерпретации в parseInt
— хотя в целях обратной совместимости основание всё-таки желательно указывать. Если нужен восьмеричный результат, основание 8
вам в помощь.
parseInt('100', '8') // <- 64
Теперь, в ES6, появился префикс 0o
для восьмеричных литералов. Можно конечно использовать и 0O
, но в некоторых шрифтах эти символы легко перепутать, так что лучше придерживайтесь нотации 0o
.
console.log(0o010) // <- 8 console.log(0o100) // <- 64
Правда, восьмеричные литералы вряд ли появятся в ваших веб-приложениях в ближайшее время, поэтому не стоит слишком переживать из-за, казалось бы, странного выбора (ради читаемости шрифта) префикса 0o
. К тому же, многие используют редакторы, которые без проблем различают 0o
, 0O
, 00
, OO
и oo
.
Если вы сейчас порываетесь недоуменно спросить «А что же насчёт шестнадцатеричных?» — не переживайте, они уже были частью языка в ES5 и их можно смело использовать. 16-ричные литералы записываются с префиксом 0x
, либо 0X
.
console.log(0x0ff) // <- 255 console.log(0xf00) // <- 3840
Ладно, надоели эти числовые литералы. Обсудим первые четыре новых метода Number
— Number.isNaN
, Number.isFinite
, Number.parseInt
и Number.parseFloat
— уже были в ES5, но в глобальном пространстве имён. Кроме того, у методов Number
есть небольшое отличие — перед получением результата они не приводят нечисловые значения к числам.
Number.isNaN
Этот метод практически идентичен глобальному методу isNaN
в ES5. Number.isNaN
проверяет, является ли указанное значение NaN
или нет. Этот вопрос отличается от «Является ли это числом?»
В кратком примере ниже показано, что всё, что не NaN
, при передаче в Number.isNaN
вернёт false
, а передав в него NaN, мы получим true
.
Number.isNaN(123) // <- false, целые числа не являются NaN Number.isNaN(Infinity) // <- false, бесконечность не является NaN Number.isNaN('ponyfoo') // <- false, 'ponyfoo' не NaN Number.isNaN(NaN) // <- true, NaN является NaN Number.isNaN('pony'/'foo') // <- true, 'pony'/'foo' является NaN, NaN является NaN
Метод global.isNaN
в ES5, наоборот, приводит переданные ему нечисловые значение перед сравнением с NaN
. И результаты от этого сильно различаются. Результаты в примере ниже противоречивы, поскольку, в отличие от Number.isNaN
, isNaN
берёт переданное ему значение и преобразует его сначала через Number
.
isNaN('ponyfoo') // <- true, поскольку Number('ponyfoo') является NaN isNaN(new Date()) // <- true
Хотя Number.isNaN
и точнее, чем его глобальный аналог isNaN
, поскольку он ничего не приводит, он по-прежнему может путать людей по ряду причин.
global.isNaN
перед сравнением приводит подаваемое ему на вход значение черезNumber(value)
Number.isNaN
не приводит- Ни
Number.isNaN
, ниglobal.isNaN
не отвечают на вопрос «Это не число?» - Они отвечают, будет ли value — или
Number(value)
— являетсяNaN
Чаще всего нам просто нужно знать, определяется ли значение как число — typeof NaN === 'number'
— и число ли это? Метод ниже это и делает. Заметье, что благодаря проверке типа это сработает как с global.isNaN
, так и с Number.isNaN
. Всё, для чего typeof
возвращает значение 'number
— число, кроме NaN
, так что мы удаляем их во избежание ложных срабатываний!
function isNumber (value) { return typeof value === 'number' && !Number.isNaN(value) }
Этот метод вычисляет, является ли что-то настоящим числом или нет. Вот некоторые примеры того, что представляет (или не представляет) собой настоящие числа в JavaScript.
isNumber(1) // <- true isNumber(Infinity) // <- true isNumber(NaN) // <- false isNumber('ponyfoo') // <- false isNumber(new Date()) // <- false
Кстати, насчет isNumber
, неужели в самом языке нет чего-то подобного? В какой-то мере.
Number.isFinite
Малоизвестный метод isFinite
появился ещё в ES3 и он возвращает, совпадает ли указанное значение со всем кроме: Infinity
, -Infinity
и NaN
.
Угадаете разницу между global.isFinite
и Number.isFinite
?
Верно! Метод
global.isFinite
приводит значения с помощьюNumber(value)
, аNumber.isFinite
— нет. Вот несколько примеров сglobal.isFinite
. В этом случае значения, которые можно привести к числам, отличным отNaN
, рассматриваются как конечные числа — даже несмотря на то, что по факту числами они не являются!
В большинстве случаев isFinite
будет достаточно, как и isNaN
, но если дело касается нечисловых значений, он начинает капризничать и производить неожиданные результаты, поскольку приводит value
к числам.
isFinite(NaN) // <- false isFinite(Infinity) // <- false isFinite(-Infinity) // <- false isFinite(null) // <- true, поскольку Number(null) равно 0 isFinite('10') // <- true, поскольку Number('10') равно 10
Number.isFinite
во всех отношениях безопаснее, поскольку не занимается лишними приведениями. А чтобы значение приводилось к его числовому представлению, всегда есть Number.isFinite(Number(value))
.
Number.isFinite(NaN) // <- false Number.isFinite(Infinity) // <- false Number.isFinite(-Infinity) // <- false Number.isFinite(null) // <- false Number.isFinite(0) // <- true
Опять же, расхождение бесполезно для языка, но Number.isFinite
однозначно полезнее isFinite
. Создание полифилла для версии Number.isFinite
в основном сводится к проверке типа.
Number.isFinite = function (value) { return typeof value === 'number' && isFinite(value) }
Number.parseInt
Этот метод идентичен parseInt
. Это вообще одно и то же.
console.log(Number.parseInt === parseInt) // <- true
Но тем не менее метод parseInt
всё равно вносит определенную путаницу — даже при том, что он не изменился, в этом-то и проблема. До ES6, parseInt
поддерживал нотацию 16-ричного литерала в строках. Даже необязательно указывать основание, parseInt
определяет это, основываясь на префиксе 0x
.
parseInt('0xf00') // <- 3840 parseInt('0xf00', 16) // <- 3840
Если жёстко вписать другое основание — и это ещё одна причина так и сделать — parseInt
проигнорирует все символы, начиная с первого нецифрового.
parseInt('0xf00', 10) // <- 0 parseInt('5xf00', 10) // <- 5, иллюстрация того, что здесь нет никакого специального подхода
Пока всё хорошо. Почему мне хочется, чтобы parseInt
отказался от 0x
в шестнадцатеричных строках? Звучит неплохо, хотя вы можете возразить, что это слишком, и, видимо, окажитесь правы.
parseInt('0b011') // <- 0 parseInt('0b011', 2) // <- 0 parseInt('0o800') // <- 0 parseInt('0o800', 8) // <- 0
Теперь мы сами должны избавляться от префикса перед parseInt
. И не забывайте жёстко прописанное основание!
parseInt('0b011'.slice(2), 2) // <- 3 parseInt('0o110'.slice(2), 8) // <- 72
А ещё удивительнее то, что метод Number
прекрасно умеет приводить эти строки к правильным числам.
Number('0b011') // <- 3 Number('0o110') // <- 72
Непонятно, из каких соображений они оставили Number.parseInt
идентичным parseInt
. Лично я сделал бы так, чтобы этот метод работал так же, как Number
— мог приводить строковые записи двоичных и восьмеричных чисел с соответствующим десятичным числам.
Возможно, для этого пришлось бы намного сильнее переделать parseInt
, чем просто отказаться от приведения входного значения к числу, как мы видели в случае Number.isNaN
и Number.isFinite
. Но здесь я могу лишь гадать.
Number.parseFloat
Аналогично parseInt
, parseFloat
остался без изменений.
Number.parseFloat === parseFloat // <- true
Однако, у parseFloat
и так ничего не менялось в отношении шестнадцатеричных литералов, так что это фактически единственный метод, который не вводит никакой путаницы, разве что только перенесён в Number
для полноты картины.
Number.isInteger
Это новый метод в ES6. Он возвращает true
, если указанное значение является конечным числом, у которой нет дробной части.
console.log(Number.isInteger(Infinity)) // <- false console.log(Number.isInteger(-Infinity)) // <- false console.log(Number.isInteger(NaN)) // <- false console.log(Number.isInteger(null)) // <- false console.log(Number.isInteger(0)) // <- true console.log(Number.isInteger(-10)) // <- true console.log(Number.isInteger(10.3)) // <- false
Если хотите взглянуть на полифилл для isInteger
, посмотрите на кусок кода ниже. Оператор деления по модулю возвращает остаток от деления одинаковых операндов, т.е. фактически: дробную часть. Если это 0
, значит число является целым.
Number.isInteger = function (value) { return Number.isFinite(value) && value % 1 === 0 }
Арифметические операции с плавающей запятой давно известны некоторыми своими несуразностями. Что ещё за Number.EPSILON
?
Number.EPSILON
Пожалуй, отвечу на этот вопрос с помощью куска кода.
Number.EPSILON // <- 2.220446049250313e-16, постойте, что? Number.EPSILON.toFixed(20) // <- '0.00000000000000022204', понял
Хорошо, значит, Number.EPSILON
— это чертовски малое число. И что с этого? Помните, что операция суммы с плавающей точкой бессмысленна? Уверен, что этот канонический пример освежит вашу память.
0.1 + 0.2 // <- 0.30000000000000004 0.1 + 0.2 === 0.3 // <- false
И ещё разок.
0.1 + 0.2 - 0.3 // <- 5.551115123125783e-17, какого чёрта? 5.551115123125783e-17.toFixed(20) // <- '0.00000000000000005551', понял
И что? Используйте Number.EPSILON
, чтобы выяснить, достаточно ли мала разница для попадания в категорию «арифметика с плавающей точкой нелепа, а разница пренебрежима».
5.551115123125783e-17 < Number.EPSILON // <- true
Этому можно верить? Ну, 0.00000000000000005551
и правда меньше 0.00000000000000022204
. В смысле, вы мне не верите? Вот же они рядом.
0.00000000000000005551 0.00000000000000022204
Видите? Number.EPSILON
больше разницы. Можно использовать Number.EPSILON
в качестве допустимой погрешности из-за операций округления арифметики с плавающей точкой.
Таким образом, в куске кода ниже выясняется, находится ли результат операции с плавающей точкой в пределах допустимой погрешности. Мы используем Math.abs
, чтобы порядок слева и справа не имел значения. Другими словами, withinErrorMargin(left, right)
вернёт тот же результат, что и withinErrorMargin(right, left)
.
function withinErrorMargin (left, right) { return Math.abs(left - right) < Number.EPSILON } withinErrorMargin(0.1 + 0.2, 0.3) // <- true withinErrorMargin(0.2 + 0.2, 0.3) // <- false
Сделать-то так можно, но, пожалуй, ни к чему такие сложности, если только вам не приходится иметь дело с очень низкоуровневой математикой. В этом случае легче внедрить в проект библиотеку
mathjs
.
И ещё кое-что важное: есть ещё один странный аспект представления числа в JavaScript. Целые числа тоже не все можно представить точно.
Number.MAX_SAFE_INTEGER
Это самое большое целое число, которое можно безопасно и точно представить в JavaScript, или любом языке, который представляет целые числа, используя плавающую точку, в соответствии со стандартом IEEE-754
. Код ниже показывает, насколько это число большое. Если нужно справиться с ещё большим числом, тогда я бы снова предложил mathjs
, либо другой язык для задач с массой вычислений.
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1 // <- true Number.MAX_SAFE_INTEGER === 9007199254740991 // <- true
И знаете, как говорится — если есть максимум…
Number.MIN_SAFE_INTEGER
Правильно, так никто не говорит. Однако, несмотря на это есть Number.MIN_SAFE_INTEGER
— и это значение Number.MAX_SAFE_INTEGER
со знаком минус.
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER // <- true Number.MIN_SAFE_INTEGER === -9007199254740991 // <- true
Как именно использовать эти две константы, спросите вы? В случае переполнения не требуется реализовывать собственный метод withinErrorMargin
, как пришлось делать для точных операций с дробями. Вместо этого есть Number.isSafeInteger
.
Number.isSafeInteger
Этот метод возвращает true для любого целого числа в диапазоне [MIN_SAFE_INTEGER, MAX_SAFE_INTEGER]
. К тому же, здесь нет приведения типов. Чтобы этот метод вернул true
, входное значение должно быть целым числом в пределах вышеупомянутых границ. Вот исчерпывающий набор примеров, чтобы понять это.
Number.isSafeInteger('a') // <- false Number.isSafeInteger(null) // <- false Number.isSafeInteger(NaN) // <- false Number.isSafeInteger(Infinity) // <- false Number.isSafeInteger(-Infinity) // <- false Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // <- false Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // <- true Number.isSafeInteger(1) // <- true Number.isSafeInteger(1.2) // <- false Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // <- true Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // <- false
Как отмечает в своей статье про числа ES6 доктор Аксель Раушмайер, чтобы проверить, находится ли результат операции в пределах границ, нужно проверить не только результат, но и оба операнда. Причина в том, что один операнд (или оба) может находиться за пределами границ, а результат быть «безопасным» (но неправильным). Аналогично, и сам результат может находиться за пределами границ, поэтому и нужна проверка левого oперанда, правого операнда и результата выражения, чтобы быть уверенными в результате.
Во всех примерах ниже результат неправильный. Вот первый пример, где оба операнда безопасны, хотя сам результат — нет.
Number.isSafeInteger(9007199254740000) // <- true Number.isSafeInteger(993) // <- true Number.isSafeInteger(9007199254740000 + 993) // <- false 9007199254740000 + 993 // <- 9007199254740992, должен быть 9007199254740993
В этом примере один из операндов не был в диапазоне, поэтому нельзя доверять точности результата.
Number.isSafeInteger(9007199254740993) // <- false Number.isSafeInteger(990) // <- true Number.isSafeInteger(9007199254740993 + 990) // <- false 9007199254740993 + 990 // <- 9007199254741982, должно быть 9007199254741983
Заметьте, что в примере выше результат вычитания находился бы в пределах границ и был бы также не точным.
Number.isSafeInteger(9007199254740993) // <- false Number.isSafeInteger(990) // <- true Number.isSafeInteger(9007199254740993 - 990) // <- true 9007199254740993 - 990 // <- 9007199254740002, должен быть 9007199254740003
Не так уж и сложно представить случай, где оба операнда выходят за границы, но результат рассматривается как «безопасный», хотя на самом деле он неправильный.
Number.isSafeInteger(9007199254740993) // <- false Number.isSafeInteger(9007199254740995) // <- false Number.isSafeInteger(9007199254740993 - 9007199254740995) // <- true 9007199254740993 - 9007199254740995 // <- -4, должно быть -2
Как видите, можно сделать вывод, что единственный способ убедиться в корректности операции — это следующий метод. Если нет гарантии, что и оба операнда, и результат операции лежат в нужных пределах, то этот результат может быть неточным, это непорядок. В таких случаях лучше всего воспользоваться оператором throw
и предусмотреть подходящий обработчик этого исключения, но это зависит от конкретной программы. Главное тут — действительно отловить такие вот хитрые баги.
function trusty (left, right, result) { if ( Number.isSafeInteger(left) && Number.isSafeInteger(right) && Number.isSafeInteger(result) ) { return result } throw new RangeError('Операции нельзя доверять!') }
Затем можно использовать это при каждом шаге, чтобы убедиться, что все операнды остаются в безопасных пределах границ. В примере ниже выделены небезопасные значения. Заметьте, что даже при том, что ни одна из операций в примере не возвращает точный результат, при некоторых операциях и числах это удаётся, даже когда операнды выходят за границы. Проблема в том, что в этом нет гарантии — поэтому операции нельзя доверять.
trusty(9007199254740000, 993, 9007199254740000 + 993) /*9007199254740000 + 993 — небезопасное значение*/ // <- RangeError: Операции нельзя доверять! trusty(9007199254740993, 990, 9007199254740993 + 990) /*9007199254740993 и 9007199254740993 + 990 — небезопасные значения*/ // <- RangeError: Операции нельзя доверять! trusty(9007199254740993, 990, 9007199254740993 - 990) /*9007199254740993 — небезопасное значение*/ // <- RangeError: Операции нельзя доверять! trusty(9007199254740993, 9007199254740995, 9007199254740993 - 9007199254740995) /*9007199254740993 и 9007199254740995 — небезопасные значения*/ // <- RangeError: Операции нельзя доверять! trusty(1, 2, 3) // <- 3
Не думаю, что я вернусь к плавающим точкам в ближайшее время. Хватит уже с меня.
Заключение
Хаки для защиты от ошибок округления и переполнения, хотя некоторые из них и полезны, не решают суть проблемы: математика со стандартом IEEE-754 сложная.
Сегодня JavaScript работает на всех устройствах и браузерах, так что было бы неплохо реализовать лучший стандарт в придачу к IEEE-754. Примерно год назад Дуглас Крокфорд придумал DEC64, но мнения варьировались от «это гениально!» до «это работа сумашедшего» — хотя мне кажется, что это нормально, когда речь заходит о большинстве материалов Крокфорда.
Было бы неплохо в итоге увидеть тот день, когда JavaScript сможет точно вычислять десятичную арифметику, а также безопасно представлять большие числа. В этот день у нас наверняка появится что-то ещё кроме плавающей точки.
P.S. Это тоже может быть интересно: