Очень странные дела: JavaScript

от автора

Никто из обычных людей не достиг в этом мире ничего значимого.
Джонатан, «Очень странные дела»

Автор материала, перевод которого мы сегодня публикуем, предлагает читателям взглянуть на необычные JavaScript-конструкции. А именно, речь пойдёт о коде, результаты работы которого могут показаться неожиданными. Разбор такого кода, по мнению автора статьи, поможет всем желающим лучше разобраться в JavaScript, в очень странном, но многими любимом языке. 


Сценарий №1: [‘1’, ‘7’, ’11’].map(parseInt)

Взглянем на следующий фрагмент кода:

['1', '7', '11'].map(parseInt); 

Можно подумать, что результатом его выполнения будет такой массив:

[1, 7, 11] 

Но на самом деле всё не так. Вот что он нам выдаст:

[1,NaN,3] 

Поначалу такой результат может показаться совершенно непонятным, но всё это вполне объяснимо. А именно, для того чтобы разобраться в том, что тут происходит, нужно как следует вникнуть в особенности работы использованных здесь механизмов языка: метода массива map() и функции parseInt().

▍Метод map()

Метод map() вызывает предоставленный ему коллбэк по одному разу для каждого элемента массива, обходя массив в порядке следования его элементов, и создаёт новый массив, содержащий результаты обработки элементов исходного массива. Коллбэк вызывается только для тех индексов массива, которым назначены какие-то значения (включая undefined).

При этом коллбэк, показанный выше, получит некоторые параметры. Изучим это на примере коллбэка, представленного методом console.log()

Здесь и далее код и результаты его выполнения представлены так, как они могут выглядеть в консоли инструментов разработчика браузера:

[1, 2, 3].map(console.log) 1 0 > (3) [1, 2, 3] 2 1 > (3) [1, 2, 3] 3 2 > (3) [1, 2, 3] 

Как видно, метод map() передаёт коллбэку не только значение элемента, но и индекс этого элемента, и сам этот массив. Этот факт очень важен, и он, отчасти, влияет на результат выполнения кода, который мы анализируем.

▍Функция parseInt()

Функция parseInt() разбирает строковой аргумент и возвращает целое число в заданной системе счисления.

Функция parseInt(string [, radix]) ожидает поступления одного обязательного параметра, строкового представления числа, и одного необязательного — основания системы счисления.

▍Раскрытие тайны

Теперь, когда мы достаточно много знаем об используемых здесь методах и функциях, попытаемся понять сущность происходящего. Начнём с исходного фрагмента кода и пошагово его разберём:

['1', '7', '11'].map(parseInt); 

Как нам известно, коллбэк, переданный map(), получит три аргумента. Поэтому перепишем код так:

['1', '7', '11'].map((currentValue, index, array) => parseInt(currentValue, index, array)); 

Вы уже начали понимать происходящее? Когда мы добавили в коллбэк аргументы, становится понятным то, что функция parseInt() получает дополнительные параметры, а не только значение элемента массива. Зная это, мы можем исследовать поведение функции в каждом из случаев. При этом тот параметр, в котором содержится исходный массив, мы можем проигнорировать, так как функция parseInt() просто не обращает на него внимания. Вот что у нас получится:

parseInt('1', 0) 1 parseInt('7', 1) NaN parseInt('11', 2) 3 

Эти результаты позволяют объяснить поведение исходного фрагмента кода. Как видно, на результат работы parseInt() влияет переданное ей основание системы счисления, от которого зависят результаты преобразования строки в число.

Можно ли изменить код так, чтобы получить ожидаемый результат — массив с результатами преобразования строк в числа?

Теперь, когда мы знаем о том, как всё это работает, мы можем, без особых сложностей, исправить код и получить желаемый результат:

['1', '7', '11'].map((currentValue) => parseInt(currentValue)); > (3) [1, 7, 11] 

Сценарий №2: (‘b’+’a’+ + ‘a’ + ‘a’).toLowerCase() === ‘banana’

Возможно, вы думаете, что в результате проверки равенства, представленного в заголовке этого раздела, получится false. В конце концов, в его левой части, там, где мы собираем строку, нет буквы n. Выясним это:

('b'+'a'+ + 'a' + 'a').toLowerCase() === 'banana' true 

Наверное, вы уже поняли причины происходящего, но, если это не так, предлагаю вместе во всём разобраться. Сосредоточимся на левой части выражения, так как в его правой части, уж поверьте на слово, ничего странного нет.

('b'+'a'+ + 'a' + 'a').toLowerCase() "banana" 

Самое интересное здесь то, как именно формируется слово banana. Поэтому давайте исследуем код формирования строки, убрав вызов метода toLowerCase(), ответственный за преобразование строки к нижнему регистру:

('b'+'a'+ + 'a' + 'a') "baNaNa" 

Вот оно! Теперь мы знаем о том, откуда тут взялись буквы N. Похоже, что в формировании строки приняло участие значение NaN. Возможно, его источником является выражение + +. Представим себе, что это так, и попробуем переписать код формирования строки следующим образом:

('b'+'a'+ NaN + 'a' + 'a') "baNaNaa" 

Как видно, тут получается вовсе не baNaNa, так как в итоговой строке появилась лишняя a. Попробуем что-нибудь другое:

+ + 'a' NaN 

Похоже, мы наконец во всём разобрались. Комбинация + + сама по себе ничего не делает, но если добавить после неё символ a, вся конструкция превращается в NaN. А это объясняет результат, полученный в исходном фрагменте кода. Значение NaN, в виде строки, конкатенируется с остальными строковыми значениями и, после приведения полученной строки к нижнему регистру, мы получаем banana.

Сценарий №3: (я даже названия для него придумать не могу)

Вот код, который я хочу тут разобрать:

(![] + [])[+[]] + (![] + [])[+!+[]] + ([![]] + [][[]])[+!+[] + [+[]]] + (![] + [])[!+[] + !+[]] === 'fail' true 

Что не так в этом мире? Как из кучи скобок получилось слово fail? И я не погрешу против истины, сказав, что такой JavaScript-код работает без ошибок и выдаёт строку fail.

Давайте с этим разберёмся. А именно, обратим внимание на одну из конструкций, которая встречается тут несколько раз:

(![] + []) 

В результате выполнения этого выражения получается false. Это странно, но это — демонстрация работы правил, на которых основан JavaScript. Так, оказывается, что истинным является следующее выражение:

false + [] === 'false' 

При вычислении этого выражения используются внутренние механизмы языка, о которых мы здесь говорить не будем.

Итак, после того, как нам удалось получить строку false, всё остальное легко: достаточно найти в полученной строке позиции нужных символов. Исключением является лишь символ i, которого в строке false нет.

Для того чтобы раздобыть букву i, используется такая конструкция, немного отличающаяся от той, которую мы уже рассмотрели:

([![]] + [][[]]) "falseundefined" 

Как видите, результатом выполнения этого выражения является строка falseundefined. Суть тут в том, что мы получаем значение undefined и конкатенируем строковое представление false со строковым представлением undefined. А всё остальное вы уже знаете.

Пока интересно? Давайте взглянем ещё на некоторые странности.

Сценарий №4: значение true и истинные значения, значение false и ложные значения

Что такое «истинные» и «ложные значения? Почему они отличаются от значений true и false?

Существуют правила, по которым разные значения в JavaScript приводятся к логическим значениям. Те значения, которые приводятся к значению true, называются истинными. Те, которые приводятся к false — ложными. Эти значения используются в операциях, в которых ожидается наличие логических значений, но такие значения этим операциям не предоставляются. Весьма вероятно то, что иногда вы пользуетесь примерно такими конструкциями:

const array = []; if (array) {   console.log('Truthy!'); } 

В вышеприведённом коде константа array не является значением логического типа, но значение, записанное в неё, является «истинным», поэтому при выполнении этого кода выводится Truthy!.

▍Истинным или ложным является значение?

Всё, что не является ложным, является истинным. Ужасное объяснение, правда? Но оно достаточно логично. Исследуем его.

Ложными являются значения, приводимые к false:

  • 0
  • -0
  • 0n
  • » или «»
  • null
  • undefined
  • NaN

Все остальные значения являются истинными.

Сценарий №5: сравнение массивов с другими значениями

Кое-что в JavaScript — это просто странно. Но эти странности закреплены в стандартах, поэтому мы принимаем их такими, какие они есть. Рассмотрим несколько примеров сравнения массивов с другими значениями:

[] == ''   // -> 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.14 Abstract Equality Comparison стандарта ECMAScript 2019. Но предупреждаю сразу: обычным людям этого лучше не видеть :-).

Сценарий №6: математика — это математика, если только не…

В обычной жизни мы знаем о том, что математика — это математика. Мы знаем о том, как работают математические операторы. Ещё детьми мы усваиваем, например, правила сложения чисел, и знаем о том, что если сложить одни и те же числа, то всегда получится один и тот же результат. Верно? Но в мире 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 

В первых строках этого фрагмента кода всё выглядит так, как ожидается, до тех пор, пока мы не доберёмся до следующего:

'3' - 1  // -> 2 '3' + 1  // -> '31' 

Когда из строки вычитают число, и строка и число ведут себя как числа. А когда к строке число прибавляют, и строка и число ведут себя как строки. Почему? Потому что язык так устроен. Вот простая таблица, которая поможет вам разобраться в том, как поведёт себя язык в каждом из случаев:

Number  + Number  -> сложение Boolean + Number  -> сложение Boolean + Boolean -> сложение Number  + String  -> конкатенация String  + Boolean -> конкатенация String  + String  -> конкатенация 

А как насчёт других примеров? Для того чтобы с ними разобраться, нужно учитывать то, что для массивов, [], и объектов, {}, перед сложением вызываются методы для преобразования их в примитивные значения. Вот разделы ECMAScript 2019, в которых можно найти подробности о вычислении подобных выражений:

Стоит отметить, что результат вычисления выражения {} + [] отличается от результата вычисления выражения [] + {}. Причина этого в том, что в первом случае пара фигурных скобок интерпретируется как блок кода. А унарный оператор + преобразует пустой массив, [], в число. В результате JavaScript-интерпретатор видит первый пример так:

{   // это - блок кода } +[]; // -> 0 

Для того чтобы это выражение дало бы тот же результат, что и [] + {}, его нужно заключить в круглые скобки:

({} + []); // -> [object Object] 

Итоги

Надеюсь, вам было так же интересно читать этот материал, как мне — его писать. JavaScript — это замечательный язык, полный неочевидных возможностей и странностей. Хочется верить, что эта статья позволила вам лучше разобраться в некоторых интересных механизмах языка, и вы, когда в следующий раз столкнётесь с чем-то подобным, будете точно знать о том, что происходит.

А вы сталкивались с какими-нибудь странностями JavaScrip

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/506566/


Комментарии

Добавить комментарий

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