Русский
Русский
English
Статистика
Реклама

Перевод Очень странные дела 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)1parseInt('7', 1)NaNparseInt('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 // -> 23 + 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
Источник: habr.com
К списку статей
Опубликовано: 14.06.2020 18:20:27
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Javascript

Разработка веб-сайтов

Разработка

Категории

Последние комментарии

© 2006-2020, personeltest.ru