Number.prototype.toFixed: удивительно корректно в Internet Explorer

Рассмотрим следующее:

var x = 2.175;
console.log(x.toFixed(2));  // 2.17

Что? Нет, не удивительно. Это довольно очевидно, см.: Литерал чисел 2.175 фактически хранится в памяти (по правилам IEEE-754) как значение, которое чуть меньше, чем 2,175. И это легко доказать:

console.log(x.toFixed(20)); // 2.17499999999999982236

Как это работает в последних версиях Firefox, Chrome и Opera при 32-разрядной установке Windows. Но это не вопрос.

Реальный вопрос заключается в том, как Internet Explorer 6 (!) действительно справляется с этим right, как это делают люди:

var x = 2.175;
console.log(x.toFixed(2));  // 2.18
console.log(x.toFixed(20)); // 2.17500000000000000000

ОК, я переоценил: на самом деле все исследователи Интернета, на которых я тестировал это (IE8-11, и даже MS Edge!), ведут себя одинаково. Тем не менее, WAT?

ОБНОВЛЕНИЕ: Он становится странным:

x=1.0;while((x-=0.1) > 0) console.log(x.toFixed(20));

IE                        Chrome
0.90000000000000000000    0.90000000000000002220
0.80000000000000000000    0.80000000000000004441
0.70000000000000010000    0.70000000000000006661
0.60000000000000010000    0.60000000000000008882
0.50000000000000010000    0.50000000000000011102
0.40000000000000013000    0.40000000000000013323
0.30000000000000015000    0.30000000000000015543
0.20000000000000015000    0.20000000000000014988
0.10000000000000014000    0.10000000000000014433
0.00000000000000013878    0.00000000000000013878

Почему разница - во всех, кроме последнего? И почему нет разницы в последнем? Это очень похоже на x=0.1; while(x-=0.01)..., между прочим: пока мы не приблизимся к нулю, toFixed в IE, по-видимому, пытается разрезать некоторые углы.

Отказ от ответственности: я do знают, что математика с плавающей запятой является некорректной. Я не понимаю, какая разница между IE и остальной частью мира браузера.

Ответ 1

Сообщаемое поведение отклоняется от требований спецификации ECMA.

В соответствии с пунктом 8.5 тип Number имеет 64-разрядные двоичные значения IEEE-754, за исключением того, что существует только один NaN. Так что 2.175 невозможно представить точно; ближайший вы можете получить: 2.17499999999999982236431605997495353221893310546875.

В соответствии с 15.7.4.5, toFixed(20) используется алгоритм, который сводится к следующему:

  • "Пусть n - целое число, для которого точное математическое значение n ÷ 10 f - x максимально приближено к нулю. Если таких двух n, выберите большее n."
  • В приведенном выше примере f равно 20 (количество запрошенных цифр), а x - операнд, который должен быть 2.17499999999999982236431605997495353221893310546875.
  • Это приводит к выбору 217499999999999982236 для n.
  • Затем n форматируется, производя "2.17499999999999982236".

Ответ 2

Я ценю вклад Эрика, но, со всем уважением, он не отвечает на вопрос. Я признаю, что был слишком наглым с этими "правильными" и "удивительно правильными" фразами; но да, я понимаю, что поведение IE - это отклонение на самом деле.

В любом случае. Я все еще искал объяснение, что заставляет IE вести себя по-другому - и я наконец получил что-то похожее на подсказку... по иронии судьбы, в трекер Mozilla, в этом longy обсуждение. Цитата:

OUTPUT IN MOZILLA: 
a = 0.827 ==> a.toFixed(17) = 0.82699999999999996 
b = 1.827 ==> b.toFixed(17) = 1.82699999999999996

OUTPUT IN IE6: 
a = 0.827 ==> a.toFixed(17) = 0.82700000000000000 
b = 1.827 ==> b.toFixed(17) = 1.82700000000000000

Разница, наблюдаемая в IE и Mozilla, такова. IE хранит 'a' как строкуи Mozilla сохраняет "a" в качестве значения. Спецификация не прибивает формат хранения. Таким образом, когда IE делает a.toFixed, он начинается с точное строковое представление, в то время как Mozilla переносит конверсии в оба конца.

Было бы здорово иметь официальное подтверждение об этом, но, по крайней мере, это объясняет все, что я видел. В частности,

console.log( 0.3.toFixed(20) ); // 0.30000000000000000000
console.log( 0.2.toFixed(20) ); // 0.20000000000000000000
console.log( (0.3 - 0.2).toFixed(20) ); // "0.09999999999999998000"

Ответ 3

Прежде всего, плавающие точки не могут быть "точно" представлены в двоичных числах. Будет высота/депрессия, либо значение будет немного выше, либо немного ниже. Насколько это повышается/депрессия зависит от того, как делается конверсия. Неверное значение не существует даже для строкового вывода ECMAScript toFixed().

Но стандарты ECMA делают все, что нужно для установления стандартов. На мой взгляд, это хорошо. Это похоже на "Если все мы все сделаем ошибки, пусть сделаем одно и то же".

Итак, теперь будет вопрос, как и почему IE отклоняется от Стандартов. Давайте рассмотрим следующие тестовые примеры.

Кандидатами являются IE 10.0.9200.16688 и Chrome 30.0.1599.69, работающие на x64 Windows 8 Pro.

Case Code                       IE (10)                        Chrome (30)
--------------------------------------------------------------------------------
A    (0.06).toFixed(20)         0.60000000000000000000    0.05999999999999999778
B    (0.05+0.01).toFixed(20)    0.06000000000000000500    0.06000000000000000472

Итак, независимо от IE или Chrome, мы видим, что (0.06) не совсем равно (0.05+0.01). Почему это? Это потому, что (0,06) имеет представление, очень близкое, но не равное (0,06), равно (0,05) и (0,01). Когда мы выполняем операцию, такую ​​как добавление, , очень менее значимые ошибки могут суммироваться, чтобы стать ошибкой немного различной величины.

Теперь разница в представленном значении в разных браузерах может быть затронута по двум причинам:

  • Используемый алгоритм преобразования.
  • Когда происходит преобразование.

Теперь мы не знаем, что использует IE IE, поскольку я не могу посмотреть в него источник. Но приведенные выше тестовые примеры ясно демонстрируют еще одну вещь: IE и Chrome обрабатывают преобразование "не только по-разному", но также " в другом случае".

В JavaScript, когда мы создаем число (например, экземпляр класса Number с ключевым словом new или без него), мы фактически предоставляем literal. Литерал всегда является строкой, даже если он обозначает число [1]. Браузер анализирует литерал и создает объект и присваивает представленное значение.

Теперь, где вещи, как правило, идут разными путями. IE удерживает конверсию до тех пор, пока она не понадобится. Это означает, что до тех пор, пока операция не будет выполнена, IE сохранит число как литерал (или некоторый промежуточный формат). Но Chrome конвертирует его прямо в рабочий формат.

После выполнения операции IE не возвращается к литераловому формату или промежуточному формату, поскольку он бессмыслен и может привести к небольшой потере точности.

Я надеюсь, что это прояснит.


[1] Значение, представленное в коде, всегда literal s. Если вы их цитируете, они называются String Literal s.