Неожиданные особенности JavaScript

Благодаря широкому распространению и совместимости с различными платформами 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».

2 комментария к “Неожиданные особенности JavaScript”

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх