Этот пост — список забавных и хитрых примеров на 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
Объяснение:
Спецификация строго определяет логику такого поведения:
- Если
Type(x)
отличается отType(y)
, то возвращается false. - Если
Type(x)
является числом, тогда
- если x является NaN, то возвращается false.
- если y является NaN, то возвращается false.
- … … …
Согласно определению 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
Объяснение:
Согласно спецификации:
- Если при вызове функции не были переданы параметры, то пусть
n
будет+0
. - Также пусть
n
будет? ToNumber (value)
. В случае с 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-комментарий
Комментариев нет:
Отправить комментарий