Перлы языка JavaScript: строки, массивы и непонятные моменты
Возвращаясь к «косякам», JavaScript строки, массивы и разномастные формулы также оказались под прицелом Брайана Лера в его лекции «WTFJS».
Мы уже много раз говорили о многофункциональности JavaScript, но все еще остаются некоторые моменты которые не до конца ясны. К ним относятся логические несостыковки в математических уравнениях, Arrays и многом другом. С одной стороны может показаться, что создатели JavaScript не учли наличие и решение таких проблем. Давайте же заглянем в спецификации и посмотрим, где действительно скрывается причина подобных моментов.
Минимальное значение больше нуля
Number.MIN_VALUE – наименьшее число, которое больше нуля:
Number.MIN_VALUE> 0 // -> true
Объяснение:
Number.MIN_VALUE = 5e-324, т. е. наименьшее положительное число, которое может быть представлено с точностью до float – как можно ближе к нулю.
Теперь самое маленькое значение – Number.NEGATIVE_INFINITY, хотя оно не является действительно числовым в строгом смысле.
- 20.1.2.9 Number.MIN_VALUE
- «Почему 0 меньше, чем минимальное значение в JavaScript?» из StackOverflow.
Функция – это не функция
Ошибка, присутствующая в V8 v5.5 или ниже (Node.js <= 7).
Как насчет такого? Посмотрите на эти JavaScript строки:
// Объявить класс, который экстендит значение null class Foo extends null {} // -> [Function: Foo] new Foo instanceof null // > TypeError: функция - не функция
Объяснение:
Это не является частью спецификации, а просто ошибка, которая теперь исправлена, поэтому в будущем с ней не должно быть проблем.
Добавление массивов
Что произойдет, если вы попытаетесь вывести сумму двух массивов?
[1, 2, 3] + [4, 5, 6] // -> '1,2,34,5,6'
Объяснение:
Конкатенация происходит. Если разбить ее на поэтапную модель, то выглядит все следующим образом:
[1, 2, 3] + [4, 5, 6] // вызываем toString () (JavaScript строки) [1, 2, 3] .toString () + [4, 5, 6] .toString () // конкатенация '1,2,3' + '4,5,6' // -> '1,2,34,5,6'
Массив с плавающей запятой
Вы создали массив с четырьмя пустыми элементами. Несмотря на это, вы получите 3 элемента из-за плавающей запятой:
let а = [,,,] a.length // -> 3 a.toString () // -> ',,'
Объяснение:
Трейлинг-запятые (также называемые «финальными запятыми») могут быть полезны при добавлении новых элементов, параметров или свойств в код JS. Хотите добавить новое свойство? Просто объявите новую строку без изменения предыдущей, если данная строка уже использует запятую. Такой подход значительно упростит работу, ведь само редактирование кода станет менее проблематичным.
- Пронумерованные запятые в MDN
Равномерность массива – монстр
Равномерность массива – это настоящий монстр в мире JS. Смотрите сами:
[] == '' // -> true [] == 0 // -> true [''] == '' // -> true [0] == 0 // -> true [0] == '' // -> false [''] == 0 // -> true [Null] == '' // true [Null] == 0 // true [Undefined] == '' // true [Undefined] == 0 // true [[]] == 0 // true [[]] == '' // true [[[[[[]]]]]] == '' // true [[[[[[]]]]]] == 0 // true [[[[[[Null]]]]]] == 0 // true [[[[[[Null]]]]]] == '' // true [[[[[Undefined]]]]]] == 0 // true [[[[[Undefined]]]]]] == '' // true
Объяснение:
Вы должны быть очень осторожны! Это сложный пример, но он описан в спецификации 7.2.13. Абстрактное сравнение равенства.
undefined и Number
Если мы не передадим аргументы в конструктор Number, то получим 0. Значение undefined присваивается формальным аргументам, когда нет реальных, поэтому вполне ожидаемо, что Number без аргументов принимает значение undefined. Если же это неопределенность, мы однозначно получим результат в виде NaN.
Number () // -> 0 Number (undefined) // -> NaN
Объяснение:
Согласно спецификации:
- Если аргументы этой функции не были переданы, пусть n будет равно +0.
- Иначе что произойдет с n? ToNumber (value).
- В случае с undefined, ToNumber (undefined) должен возвращать NaN.
Также рекомендуем изучить соответствующие разделы:
parseInt – плохой парень
parseInt славится своими причудами:
parseInt ( 'е*ск'); // -> NaN parseInt ('f*ck', 16); // -> 15
Объяснение:
Это происходит потому, что parseInt будет производить посимвольный синтаксический разбор до тех пор, пока не наткнется на символ, которого он не знает. f в 'f *ck' является шестнадцатеричной цифрой 15.
Парсинг Infinity в данной ситуации представляет собой нечто подобное:
// parseInt ('Infinity', 10) // -> NaN // ... parseInt ('Infinity', 18) // -> NaN ... parseInt ('Infinity', 19) // -> 18 // ... parseInt ('Infinity', 23) // -> 18 ... parseInt ('Infinity', 24) // -> 151176378 // ... parseInt ('Infinity', 29) // -> 385849803 parseInt ('Infinity', 30) // -> 13693557269 // ... parseInt ('Infinity', 34) // -> 28872273981 parseInt ('Infinity', 35) // -> 1201203301724 parseInt ('Infinity', 36) // -> 1461559270678 ... parseInt ('Infinity', 37) // -> NaN
Также будьте осторожны нулевым значением:
parseInt (null, 24) // -> 23
Объяснение:
Да, JavaScript строки нередко ведут себя крайне странно: привыкайте. Приведенный выше пример конвертирует значение null в строку «null» и пытается его преобразовать. В интервале от 0 до 23 нет цифр, которые он может преобразовать, поэтому он возвращает NaN.
- "parseInt (null, 24) === 23 ... подождите, что?" из StackOverflow
Не забывайте и о восьмеричных значениях:
parseInt ('06'); // 6 parseInt ('08'); // 8 поддерживается ECMAScript 5 parseInt ('08'); // 0, не поддерживается ECMAScript 5
Объяснение:
Если входная строка начинается с «0», то все зависит от поддержки ECMAScript 5, что возможно не во всех современных браузерах.
parseInt всегда конвертирует ввод в строку:
parseInt ({toString: () => 2, valueOf: () => 1}) // -> 2 Number ({toString: () => 2, valueOf: () => 1}) // -> 1
Математика с true и false
Давайте сгенерируем небольшой математический пример:
True + true // -> 2 (True + true) * (true + true) - true // -> 3
Хммм...
Объяснение:
Мы можем закреплять за значениями числа с помощью конструктора Number. Совершенно очевидно, что true будет равно единице:
Number (true) // -> 1
Оператор унарного плюса пытается преобразовать его значение в число. Он может преобразовывать JavaScript строки, с помощью которых реализовано представление целых чисел, и float. А еще, как видим, это могут быть значения, отличные от строки: true, false и null. Если он не может разобрать конкретное значение, в результате будет выдан NaN. Это означает, что мы можем все упростить:
+ true // -> 1
Когда вы выполняете сложение или умножение, вызывается метод ToNumber. Согласно спецификации, этот метод возвращает:
- Если аргумент равен true, возвращается 1.
- Если аргумент false, возвращается +0.
Вот почему мы можем добавлять логические значения в виде регулярных чисел и получать правильные результаты.
Соответствующие разделы по теме:
HTML-комментарии возможны в JavaScript
Вы удивитесь, но <!-- (который известен как комментарий HTML) является допустимым комментарием в JavaScript.
// действительный комментарий <!-- тоже действительный комментарий
Объяснение:
Впечатлены? HTML-подобные комментарии предназначены для того, чтобы браузеры, которые не понимают тег <script>, быстрее загибались. К таким можно отнести Netscape 1.x, который давно перестал быть популярным. Поэтому нет смысла добавлять HTML-комментарии в теги скриптов.
Поскольку Node.js основан на движке V8, комментарии, подобные HTML, также поддерживаются средой выполнения Node.js. Более того, они являются частью спецификации:
NaN не является числом
Тип NaN – это число:
typeof NaN // -> 'number'
Объяснения того, как работают операторы typeof и instanceof:
[] И null – объекты
typeof [] // -> 'object' typeof null // -> 'object' // Однако Null instanceof Object // false
Объяснение:
Поведение оператора typeof подробно разъясняется в этом разделе спецификации:
Согласно спецификации, оператор typeof возвращает JavaScript строки в соответствии с таблицей 35: typeof Operator Results. Для нулевых, обычных и многих других объектов, которые не реализовывают [[Call]], он возвращает строку «object».
Тем не менее, вы можете проверить тип объекта с помощью метода toString.
Object.prototype.toString.call ([]) // -> '[object Array]' Object.prototype.toString.call (новая дата) // -> '[object Date]' Object.prototype.toString.call (нуль) // -> '[object Null]'
Магически увеличивающиеся числа
999999999999999 // -> 999999999999999 9999999999999999 // -> 10000000000000000 10000000000000000 // -> 10000000000000000 10000000000000000 + 1 // -> 10000000000000000 10000000000000000 + 1.1 // -> 10000000000000002
Объяснение:
Это вызвано стандартом IEEE 754-2008 для двоичной системы с плавающей запятой. В данном масштабе все округляется до ближайшего четного числа. Прочитайте больше:
- 6.1.6 Тип номера
- IEEE 754 в Википедии
Точность 0.1 + 0.2
Известная шутка. Добавление 0.1 и 0.2 является смертельно точным:
0.1 + 0.2 // -> 0.30000000000000004 (0.1 + 0.2) === 0.3 // -> false
Объяснение:
Ответ на вопрос «Связана ли эта проблема с плавающей запятой?» на StackOverflow:
Константы 0.2 и 0.3 в вашей программе также будут приближаться к их истинным значениям. Бывает, что ближайший double к 0.2 больше, чем рациональное число 0.2, но ближайший double к 0.3 меньше рационального числа 0.3. Сумма 0.1 и 0.2 больше, чем рациональное число double и, следовательно, не согласуется с константой в вашем коде.
Эта проблема настолько известна, что есть даже сайт под названием 0.30000000000000004.com. Такое встречается в каждом языке, который использует математику с плавающей запятой, а не только JavaScript.
Патч-номера
Вы можете добавить свои собственные методы для обертки таких объектов, как Number или String.
Number.prototype.isOne = function () { return Number (this) === 1 } 1.0.isOne () // -> true 1..isOne () // -> true 2.0.isOne () // -> false (7) .isOne () // -> false
Объяснение:
Очевидно, вы можете экстендить объект Number, как и любой другой объект в JavaScript. Однако не рекомендуется это делать, если поведение определенного метода не является частью спецификации. Вот список свойств Number:
Сравнение трех чисел
1 <2 <3 // -> true 3> 2> 1 // -> false
Объяснение:
Почему так? Ну, проблема в первой части выражения. Вот как это работает:
1 <2 <3 // 1 <2 -> true true <3 // true -> 1 1 <3 // -> true 3> 2> 1 // 3> 2 -> true true > 1 // true -> 1 1> 1 // -> false
Мы можем исправить это с помощью оператора больше или равно (> =):
3> 2> = 1 // true
Подробнее о реляционных операторах в спецификации:
Смешная математика
Часто результаты арифметических операций в JavaScript могут быть довольно неожиданными. Рассмотрим следующие примеры:
3 - 1 // -> 2 3 + 1 // -> 4 '3' - 1 // -> 2 '3' + 1 // -> '31' '' + '' // -> '' [] + [] // -> '' {} + [] // -> 0 [] + {} // -> '[object Object]' {} + {} // -> '[object Object][object Object]' '222' - -'111' // -> 333 [4] * [4] // -> 16 [] * [] // -> 0 [4, 4] * [4, 4] // NaN
Объяснение:
Что происходит в первых четырех примерах? Вот небольшая таблица, чтобы понять принцип добавления в языке JavaScript:
Number + Number -> addition Boolean + Number -> addition Boolean + Boolean -> addition Number + String -> concatenation String + Boolean -> concatenation String + String -> concatenation
Как насчет других примеров? Методы ToPrimitive и ToString неявно вызываются для [] и {} перед добавлением. Подробнее о процессе оценки в спецификации:
- 12.8.3 Оператор добавления (+)
- 7.1.1 ToPrimitive (input [,PreferredType])
- 7.1.12 ToString (argument) (JavaScript строки)
JavaScript строки не являются экземплярами String
'str' // -> 'str' typeof 'str' // -> 'string' 'str' instanceof String // -> false
Объяснение:
Конструктор возвращает строку:
typeof String ('str') // -> 'string' String ('str') // -> 'str' String ('str') == 'str' // -> true
Попробуем с новым кодом:
new String ('str') == 'str' // -> true typeof new String ('str') // -> 'object'
JavaScript строки – объект? Что это?
new String ('str') // -> [String: 'str']
Дополнительная информация о конструкторе String в спецификации:
- 21.1.1 Строковый конструктор (JavaScript строки)