Благодаря широкому распространению и совместимости с различными платформами JavaScript стал основополагающим языком в современной веб-разработке. Но это также сложный язык со множеством неожиданных вывертов. В этой статье мы рассмотрим особенности JavaScript, способные по-настоящему озадачить.
Готовы к выносу мозга?
Оператор == и приведение типов
2 == [2] // true
Оператор == в JS выполняет приведение типов. Это означает, что перед выполнением сравнения он пытается привести сравниваемые значения к общему типу данных.
В нашем примере и число 2, и массив [2] преобразуются в строки. В результате оба значения равны 2. Поэтому в результате сравнения мы получаем true.
Обычно рекомендуется использовать оператор строгого равенства === вместо ==, чтобы избежать неожиданных результатов из-за приведения типов.
Другие примеры:
'123' == 123 // true 'foo' == NaN // false undefined == null // true NaN === NaN // false NaN == NaN // false 0 == null // false
Сравнение пустых массивов
[] == ![] // true
Здесь мы сравниваем пустой массив с булевым значением, которое было создано путем отрицания пустого массива с помощью оператора !
. Результатом сравнения является true, что может показаться неожиданным на первый взгляд…
В JS каждое значение может быть либо истинным, либо ложным в булевом контексте. Пустой массив является истинным значением, то есть в булевом контексте он считается true. Когда мы применяем к нему оператор !
, значение преобразуется в false.
С другой стороны, булево значение, созданное отрицанием непустого массива, — false. Когда мы сравниваем пустой массив со значением false с помощью оператора ==, JS пытается привести значения к общему типу перед их сравнением. При этом пустой массив преобразуется в false, в результате чего обе стороны будут ложными. В конце сравнение возвращает true.
Другие интересные примеры:
['a', 'b'] !== ['a', 'b'] // true ['a', 'b'] == ['a', 'b'] // false [1, 2] + [3, 4] // "1,23,4"
Сравнение null и undefined
null == undefined // true
Оператор == используется для сравнения двух значений на равенство. При этом он игнорирует типы данных сравниваемых значений. Если сравнивать null и undefined с помощью оператора ==, в результате получим true. Дело в том, что и null, и undefined представляют собой отсутствие значения и поэтому эквивалентны друг другу в данном контексте.
А вот с оператором строгого равенства мы получим false:
null === undefined // false
typeof
typeof NaN // number typeof null // object
В JS typeof
— это оператор, используемый для определения типа значения или переменной.
NaN означает Not a Number («не число») и является специальным значением, представляющим неопределенное или непредставимое числовое значение.
Когда вы используете typeof
с NaN, он вернет число. Это может показаться странным, но в JS NaN технически является числовым типом данных, даже если он представляет то, что на самом деле не является числом.
Когда typeof
применяется к null, он возвращает строковый объект. Это происходит потому, что null считается специальным значением, которое представляет собой пустую ссылку на объект. null — это не объект, а скорее примитивное значение. Это считается одной из странностей JavaScript.
Другие примеры:
typeof function(){} // "function" null instanceof Object // false
Сравнение булевых значений
true == "1" // true false == "0" // true
JS преобразует строку 1 в булево значение true, а строку 0 — в false. Таким образом, сравнение преобразуется в true == true
, что дает true, и false == false
, что тоже дает true.
Другие примеры:
1 + true // 2 1 - true // 0 '' == false // true 0 == false // true true + false // 1
Сложение и вычитание строк и чисел
"1" + 1 // "11" 2 + "2" // "22" "5" - 3 // 2
Когда вы используете оператор + со строкой и числом, число преобразуется в строку и конкатенируется со строкой.
Если строка может быть распознана как число, то число вычитается из строки.
Таким образом,
"1" + 1
становится строкой «11»2 + "2"
становится строкой «22»"5" - 3
превращается в число 2
Другие примеры:
+"1" // 1 -"1" // -1 +true // 1 -true // -1 +false // 0 -false // -0 +null // 0 +undefined // NaN 1 / "2" // 0.5 "2" / 1 // 2 1 / 0 // Infinity -1 / 0 // -Infinity 3 * "abc" // NaN true > false // true undefined + 1 // NaN undefined - 1 // NaN undefined - undefined // NaN undefined + undefined // NaN null + 1 // 1 null - 1 // -1 null - null // 0 null + null // 0 Infinity + 1 // Infinity Infinity - 1 // Infinity Infinity - Infinity // NaN Infinity + Infinity // Infinity Infinity / Infinity // NaN
baNaNa
Здесь происходит конкатенация строки b
, строки a
, строки, полученной из выражения +"a"
, и строки a
.
Выражение +"a"
превращает строку a
в число, которое оценивается как NaN, поскольку a не является валидным числом.
При конкатенации b
, a
, NaN
и a
мы получаем строку baNaNa
.
Сравнение пустых объектов
!{} // false {} == !{} // false {} == {} // false
Тут мы сравниваем пустой объект {}
с отрицаемым пустым объектом !{}
. Восклицательный знак !
— это логический оператор НЕ, который отрицает значение объекта. Поскольку объект в JavaScript считается истинным, !{}
возвращает false. Фактически мы сравниваем {}
с false, что приводит к ложному значению, поскольку они не равны по значению или типу данных.
В последнем выражении мы сравниваем два пустых объекта {}
. Несмотря на то, что они могут казаться одинаковыми, это два отдельных объекта с разными ссылками в памяти, поэтому они не равны по значению или типу данных. В итоге сравнение также дает нам false.
Когда вы используете оператор сложения + между двумя объектами, заключенными в фигурные скобки {}
, он пытается объединить объекты как строки.
Другие примеры:
{} + [] === "" // false !!{} // true !![] // true [] + [] // "" [] + {} // "[object Object]" {} + [] // "[object Object]" {} + {} // "[object Object][object Object]" [] == false // true !!'' // false !!0 // false !!null // false !!undefined // false
Операторы больше/меньше
7 > 6 > 5 // false
Сначала 7 > 6
оценивается как true, потому что 7 больше 6.
Затем оценивается true > 5
. В JS true преобразуется в число 1, а false — в 0. Таким образом, 1 > 5
дает false, поскольку 1 не больше 5.
В итоге 7 > 6 > 5
эквивалентно true > 5
, что ложно.
Другие примеры:
5 < 6 < 7 // true 0 > null // false
Math.max() и Math.min()
Math.max() // -Infinity Math.min() // Infinity
Функции Math.max()
и Math.min()
можно использовать для нахождения наибольшего и наименьшего значений в наборе чисел.
При вызове без каких-либо аргументов Math.max()
возвращает -Infinity, что представляет собой наименьшее возможное число в JS. А Math.min()
возвращает Infinity
, что представляет собой наибольшее возможное число в JS.
Такое поведение имеет смысл, потому что если не предоставлены числа, то Math.max()
не может вернуть наибольшее, а Math.min()
— наименьшее число.
parseInt()
parseInt('08') // 8 parseInt('08', 10) // 8 parseInt('0x10') // 16
parseInt('08')
преобразует строку 08
в целое число 8. Если написать parseInt('08', 10)
, функция все равно вернет 8.
Это происходит потому, что второй параметр функции parseInt
определяет используемую систему счисления (двоичную, восьмеричную, десятичную, шестнадцатеричную и т.д.). Если этот параметр не указан, parseInt
попытается определить систему счисления на основе формата строки. В приведенном выше примере 08
считается восьмеричным числом, поскольку оно начинается с 0, поэтому оно преобразуется в десятичное число 8.
parseInt('0x10')
преобразует шестнадцатеричную строку 0x10
в целое число 16. Система счисления также не указана, но префикс 0x
указывает на то, что число должно рассматриваться как шестнадцатеричное, поэтому оно преобразуется в десятичное число 16.
Другие примеры:
parseFloat('3.14.15') // 3.14 parseFloat('0.0') // 0
Удаление переменной внутри функции
(function(x) { delete x; return x; })(1); // 1
Анонимная функция принимает аргумент x
. Внутри функции мы пытаемся удалить переменную x
. Но это невозможно, поскольку x
является аргументом функции и не может быть удалена. Затем функция возвращает значение x
.
Когда эта функция вызывается с аргументом 1, значение x
внутри функции устанавливается в 1. Операция удаления не имеет эффекта, функция просто возвращает значение x
, равное 1.
Дополнительно
for (var i = 0; i < 3; ++i) { setTimeout(() => console.log(i), 1000); // returns 3 three times } for (let i = 0; i < 3; ++i) { setTimeout(() => console.log(i), 1000); // returns 0 1 2 }
var
создает связывание в области видимости функции, поэтому после односекундного тайм-аута цикл уже завершился. Мы получаем число 3 трижды. Используя let
, мы привязываем переменную в области видимости блока (цикла). Поэтому и получаем ожидаемые значения, так как i
ссылается на значение в данной итерации цикла.
От редакции Techrocks: о var и let читайте в статье «Var, Let и Const: в чем разница?».
Перевод статьи «Unexpected Moments of JavaScript That Will Challenge Your Understanding of the Language».
В сравнении пустых объектов же
{} + [] // 0
{} + {} // NaN
В сравнении пустых объектов же
{} + [] // 0
{} + {} // NaN