...

вторник, 15 августа 2017 г.

[Перевод] Что за черт, Javascript

Этот пост — список забавных и хитрых примеров на JavaScript. Это отличный язык. У него простой синтаксис, большая экосистема и, что гораздо важнее, огромное сообщество.

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


Содержание



Мотивация


Ради удовольствия
“Just for Fun: The Story of an Accidental Revolutionary”, Линус Торвальдс

Главная цель появления этого списка — собрать несколько безумных примеров и по возможности объяснить, как они работают. Просто потому, что это приятно, узнавать что-то, о чём мы раньше не знали.

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


Нотация

// -> используется для отображения результата выражения. Например:

1 + 1 // -> 2

// > означает результат console.log или другие выходные данные. Например:

console.log('hello, world!') // -> hello, world!

// всего лишь комментарий для объяснений. Например:

// Присвоение функции константе foo
const foo = function () {}

Примеры


[] эквивалентно ![]

Массив эквивалентен не массиву:

[] == ![] // -> true

Объяснение:



true это false

!!'false' ==  !!'true'  // -> true
!!'false' === !!'true' // -> true

Объяснение:

Рассмотрим пошагово:

true == 'true'    // -> true
false == 'false'  // -> false

// 'false' не является пустой строкой, так что это «истинноватое» (truthy) значение

!!'false' // -> true
!!'true'  // -> true


baNaNa

'b' + 'a' + + 'a' + 'a'

Это олдскульная шутка на JavaScript, но переработанная. Вот исходник:

'foo' + + 'bar' // -> 'fooNaN'

Объяснение:

Выражение вычисляется как 'foo' + (+'bar'), то есть 'bar' преобразуется в нечисловое значение.



NaN не является NaN

NaN === NaN // -> false

Объяснение:

Спецификация строго определяет логику такого поведения:


  1. Если Type(x) отличается от Type(y), то возвращается false.
  2. Если Type(x) является числом, тогда
    1. если x является NaN, то возвращается false.
    2. если y является NaN, то возвращается false.
    3. … … …

Согласно определению NaN в IEEE:


Возможно четыре взаимоисключающих отношения: меньше чем, эквивалентно, больше чем, неупорядоченно (unordered). Последний вариант бывает, когда как минимум один операнд является NaN. Каждый NaN будет неупорядоченно сравниваться со всеми, включая самого себя.
“What is the rationale for all comparisons returning false for IEEE754 NaN values?”

Это fail

Вы не поверите, но…

(![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]
// -> 'fail'

Объяснение:

Если разбить эту кучу символов на части, то можно заметить часто повторяющийся паттерн:

(![]+[]) // -> 'false'
![]      // -> false

Попробуем добавить [] к false. Но из-за нескольких внутренних вызовов фукнций (binary + Operator -> ToPrimitive -> [[DefaultValue]]) мы в результате преобразуем правый операнд в строку:

(![]+[].toString()) // 'false'

Если считать строку массивом, то можно обратиться к первому символу с помощью [0]:

'false'[0] // -> 'f'

Остальное очевидно, но с i не всё так просто. Символ i в значении fail получается с помощью генерирования строки 'falseundefined' и вытаскивания элемента с индексом ['10']


[] является «истинноватым», но не true

Массив является «истинноватым» (truthy) значением, которое, однако, не эквивалентно true.

!![]       // -> true
[] == true // -> false

Объяснение:

Вот ссылки на соответствующие разделы спецификации ECMA-262:



null является «ложноватым», но не false

Несмотря на то, что null является «ложноватым» (falsy) значением, оно не эквивалентно false.

!!null        // -> false
null == false // -> false

В то же время другие «ложноватые» значения, вроде 0 или '', эквивалентны false.

0 == false  // -> true
'' == false // -> true

Объяснение:

Объяснение то же, что и в предыдущем пример:



Минимальное значение больше нуля

Number.MIN_VALUE является самым маленьким числом больше нуля:

Number.MIN_VALUE > 0 // -> true

Объяснение:

Number.MIN_VALUE — это 5e-324, то есть самое маленькое положительное число, которое можно выразить с точностью плавающей запятой (float precision), то есть находящееся ближе всех к нулю. Это наилучшая точность, обеспечиваемая плавающей запятой.

А вообще наименьшее возможное значение — Number.NEGATIVE_INFINITY, не являющееся числовым в строгом смысле слова.



Функция не является функцией

Этот баг присутствует в движке V8 v5.5 и ниже (Node.js <=7). Все вы знаете о раздражающей ошибке «undefined is not a function», а что насчёт этого?

// Объявляем класс, расширяющий null
class Foo extends null {}
// -> [Function: Foo]

new Foo instanceof null
// -> TypeError: функция не является функцией
// ->     at … … …

Объяснение:

Это не является частью спецификации. Это просто баг, который уже исправили, так что в будущем проблем быть не должно.


Сложение массивов

Что если попробовать сложить два массива?

[1, 2, 3] + [4, 5, 6]  // -> '1,2,34,5,6'

Объяснение:

Выполняется конкатенация. Разберём пошагово:

[1, 2, 3] + [4, 5, 6]
// вызывается toString()
[1, 2, 3].toString() + [4, 5, 6].toString()
// конкатенация
'1,2,3' + '4,5,6'
// ->
'1,2,34,5,6'

Висящие запятые в массивах

Вы создали массив из четырёх пустых элементов. Но из-за висящих запятых получили массив из трёх элементов:

let a = [,,,]
a.length     // -> 3
a.toString() // -> ',,'

Объяснение:

Висящие запятые (иногда их называют «замыкающие запятые») могут быть полезны при добавлении в JavaScript-код новых элементов, параметров или свойств. Если вы хотите добавить новое свойство, то просто добавляете новую строку без изменения предыдущей последней строки, если в этой строке уже есть висящая запятая. Это упрощает управление версиями, а редактирование кода может быть менее проблематичным.

Висящие запятые


Эквивалентность массивов — это чудовище

Эквивалентность массивов в 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 в качестве значений параметров. Но если передать undefined, получим NaN.

Number()          // -> 0
Number(undefined) // -> NaN

Объяснение:

Согласно спецификации:


  1. Если при вызове функции не были переданы параметры, то пусть n будет +0.
  2. Также пусть n будет ? ToNumber (value).
  3. В случае с undefined, ToNumber(undefined) должно возвращать NaN.

Соответствующие разделы:



Плохой parseInt

parseInt славится своими причудами:

parseInt('f*ck');     // -> 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

Также будьте осторожны с разбором null:

parseInt(null, 24) // -> 23

Объяснение:

null преобразуется в строку "null", а потом пытается его конвертировать. Но для оснований от 0 до 23 не существует чисел, в которые машина может конвертировать это слово, поэтому возвращается NaN. На позиции 24 находится "n", 14-я буква латинского алфавита, она складывается с числом. На позиции 31 находится "u", 21-я буква, она тоже складывается, после чего можно декодировать всю строку. На позиции 37 уже не получится сгенерировать валидное число, поэтому возвращается NaN.

“parseInt(null, 24) === 23… погодите, что?”

Не забывайте и о восьмеричные основания (octal):

parseInt('06'); // 6
parseInt('08'); // 8 if support ECMAScript 5
parseInt('08'); // 0 if not support ECMAScript 5

Объяснение:

Если входная строка начинается с "0", то основание равно 8 (восьмеричное) или 10 (десятичное). Конкретный выбор зависит от реализации. ECMAScript 5 определяет использование 10, но оно поддерживается ещё не всеми браузерами. Поэтому всегда указывайте основание при использовании parseInt.

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 превратится в 1:

Number(true) // -> 1

Унарный оператор сложения тоже пытается превратить значение в число. Он может преобразовывать строковые представления целых чисел и чисел с плавающей запятой, а также нестроковые значения true, false и null. Если он не может разобрать конкретное значение, то вычисляет его как NaN. Так что мы можем ещё проще преобразовать true в 1:

+true // -> 1

Когда выполняется сложение или умножение, вызывается метод ToNumber. Согласно спецификации, этот метод возвращает:


Если argument равен true, то возвращается 1. Если argument равен false, то возвращается +0.

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

Соответствующие разделы:



HTML-комментарии валидны в JavaScript

Вы будете удивлены, но HTML-комментарий

Let's block ads! (Why?)

Комментариев нет:

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