Список забавных и хитрых примеров JavaScript.
JavaScript — отличный язык. У него простой синтаксис, большая экосистема и, что самое главное, отличное сообщество.
В то же время мы все знаем, что JavaScript — довольно забавный язык со сложными моментами. Некоторые из них могут быстро превратить нашу повседневную работу в ад, а некоторые — заставить нас громко смеяться.
Оригинальная идея WTFJS принадлежит Брайану Леру. Этот список во многом вдохновлен его докладом «WTFJS» на dotJS 2012:
Вы можете установить это руководство с помощью npm
. Просто запустите:
$ npm install -g wtfjs
Теперь вы сможете запускать wtfjs
из командной строки. Это откроет руководство в выбранном вами $PAGER
. В противном случае вы можете продолжить чтение здесь.
Исходный код доступен здесь: https://github.com/denysdovhan/wtfjs.
На данный момент существуют следующие переводы wtfjs :
Помогите перевести на ваш язык
Примечание. Переводы поддерживаются их переводчиками. Они могут не содержать все примеры, а существующие примеры могут быть устаревшими.
[]
равно ![]
true
не равно ![]
, но и не равно []
NaN
это не NaN
Object.is()
и ===
странные случаи[]
правдиво, но true
null
является ложным, но не false
document.all
— это объект, но он не определенundefined
и Number
parseInt
— плохой пареньtrue
и false
NaN
— это[]
и null
являются объектами0.1 + 0.2
String
constructor
__proto__
`${{Object}}`
try..catch
arguments
и стрелочные функцииNumber.toFixed()
отображает разные числаMath.max()
меньше, чем Math.min()
null
с 0
{}{}
не определенarguments
обязательныеalert
из адаsetTimeout
true
Просто для удовольствия
— «Просто ради развлечения: история случайного революционера» , Линус Торвальдс
Основная цель этого списка — собрать несколько сумасшедших примеров и, если возможно, объяснить, как они работают. Просто потому, что интересно узнавать что-то, чего мы раньше не знали.
Если вы новичок, вы можете использовать эти заметки, чтобы глубже погрузиться в JavaScript. Я надеюсь, что эти заметки побудят вас потратить больше времени на чтение спецификации.
Если вы профессиональный разработчик, вы можете рассматривать эти примеры как отличный справочник по всем причудам и неожиданным сторонам нашего любимого JavaScript.
В любом случае, просто прочитайте это. Вероятно, вы найдете что-то новое.
️ Примечание. Если вам нравится читать этот документ, пожалуйста, поддержите автора этого сборника.
// ->
используется для отображения результата выражения. Например:
1 + 1 ; // -> 2
// >
означает результат console.log
или другой вывод. Например:
console . log ( "hello, world!" ) ; // > hello, world!
//
это просто комментарий, используемый для пояснений. Пример:
// Assigning a function to foo constant
const foo = function ( ) { } ;
[]
равно ![]
Массив равен, а не массиву:
[ ] == ! [ ] ; // -> true
Оператор абстрактного равенства преобразует обе стороны в числа для их сравнения, и обе стороны по разным причинам становятся числом 0
. Массивы правдивы, поэтому справа противоположное истинному значению — false
, которое затем приводится к 0
. Однако слева пустой массив приводится к числу, не становясь сначала логическим, а пустые массивы приводятся к 0
, несмотря на то, что они правдивы.
Вот как упрощается это выражение:
+ [ ] == + ! [ ] ;
0 == + false ;
0 == 0 ;
true ;
См. также []
— это правда, но не true
.
!
)true
не равно ![]
, но и не равно []
Array не равно true
, но и не Array не равно true
; Массив равен false
, но не Array тоже равен false
:
true == [ ] ; // -> false
true == ! [ ] ; // -> false
false == [ ] ; // -> true
false == ! [ ] ; // -> true
true == [ ] ; // -> false
true == ! [ ] ; // -> false
// According to the specification
true == [ ] ; // -> false
toNumber ( true ) ; // -> 1
toNumber ( [ ] ) ; // -> 0
1 == 0 ; // -> false
true == ! [ ] ; // -> false
! [ ] ; // -> false
true == false ; // -> false
false == [ ] ; // -> true
false == ! [ ] ; // -> true
// According to the specification
false == [ ] ; // -> true
toNumber ( false ) ; // -> 0
toNumber ( [ ] ) ; // -> 0
0 == 0 ; // -> true
false == ! [ ] ; // -> true
! [ ] ; // -> false
false == false ; // -> true
! ! "false" == ! ! "true" ; // -> true
! ! "false" === ! ! "true" ; // -> true
Рассмотрим это шаг за шагом:
// true is 'truthy' and represented by value 1 (number), 'true' in string form is NaN.
true == "true" ; // -> false
false == "false" ; // -> false
// 'false' is not the empty string, so it's a truthy value
! ! "false" ; // -> true
! ! "true" ; // -> true
"b" + "a" + + "a" + "a" ; // -> 'baNaNa'
Это старая шутка в 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 .- … … …
— 7.2.14 Сравнение строгого равенства
Следуя определению NaN
из IEEE:
Возможны четыре взаимоисключающих отношения: меньше, равно, больше и неупорядоченное. Последний случай возникает, когда хотя бы один операнд имеет значение NaN. Каждый NaN должен сравнивать неупорядоченный со всем, включая самого себя.
— «Каково обоснование того, что все сравнения возвращают ложь для значений IEEE754 NaN?» в StackOverflow
Object.is()
и ===
странные случаи Object.is()
определяет, имеют ли два значения одинаковое значение или нет. Он работает аналогично оператору ===
но есть несколько странных случаев:
Object . is ( NaN , NaN ) ; // -> true
NaN === NaN ; // -> false
Object . is ( - 0 , 0 ) ; // -> false
- 0 === 0 ; // -> true
Object . is ( NaN , 0 / 0 ) ; // -> true
NaN === 0 / 0 ; // -> false
На жаргоне JavaScript NaN
и NaN
— это одно и то же значение, но они не являются строго равными. NaN === NaN
, очевидно, обусловлена историческими причинами, поэтому, вероятно, было бы лучше принять это как есть.
Аналогично, -0
и 0
строго равны, но это не одно и то же значение.
Более подробную информацию о NaN === NaN
см. в приведенном выше случае.
Вы не поверите, но…
( ! [ ] + [ ] ) [ + [ ] ] +
( ! [ ] + [ ] ) [ + ! + [ ] ] +
( [ ! [ ] ] + [ ] [ [ ] ] ) [ + ! + [ ] + [ + [ ] ] ] +
( ! [ ] + [ ] ) [ ! + [ ] + ! + [ ] ] ;
// -> 'fail'
Разбивая эту массу символов на части, мы замечаем, что часто встречается следующая закономерность:
! [ ] + [ ] ; // -> 'false'
! [ ] ; // -> false
Итак, мы пытаемся добавить []
к false
. Но из-за ряда вызовов внутренних функций ( binary + Operator
-> ToPrimitive
-> [[DefaultValue]]
) мы в конечном итоге преобразуем правильный операнд в строку:
! [ ] + [ ] . toString ( ) ; // 'false'
Думая о строке как о массиве, мы можем получить доступ к ее первому символу через [0]
:
"false" [ 0 ] ; // -> 'f'
Остальное очевидно, но с i
сложнее. i
в fail
захватывается путем создания строки 'falseundefined'
и захвата элемента по индексу ['10']
.
Еще примеры:
+ ! [ ] // -> 0
+ ! ! [ ] // -> 1
! ! [ ] // -> true
! [ ] // -> false
[ ] [ [ ] ] // -> undefined
+ ! ! [ ] / + ! [ ] // -> Infinity
[ ] + { } // -> "[object Object]"
+ { } // -> NaN
[]
правдиво, но true
Массив представляет собой истинное значение, однако он не равен true
.
! ! [ ] // -> true
[ ] == true // -> false
Вот ссылки на соответствующие разделы спецификации ECMA-262:
!
)null
является ложным, но не false
Несмотря на то, что null
— это ложное значение, оно не равно false
.
! ! null ; // -> false
null == false ; // -> false
В то же время другие ложные значения, такие как 0
или ''
равны false
.
0 == false ; // -> true
"" == false ; // -> true
Объяснение такое же, как и для предыдущего примера. Вот соответствующая ссылка:
document.all
— это объект, но он не определен
️ Это часть API браузера и не будет работать в среде Node.js.️
Несмотря на то, что document.all
— это объект, подобный массиву, и он предоставляет доступ к узлам DOM на странице, он реагирует на функцию typeof
как undefined
.
document . all instanceof Object ; // -> true
typeof document . all ; // -> 'undefined'
В то же время document.all
не равен undefined
.
document . all === undefined ; // -> false
document . all === null ; // -> false
Но в то же время:
document . all == null ; // -> true
document.all
раньше был способом доступа к элементам DOM, особенно в старых версиях IE. Хотя он никогда не был стандартом, он широко использовался в старом JS-коде. Когда в стандарте появились новые API (например,document.getElementById
), этот вызов API стал устаревшим, и комитету по стандартизации пришлось решать, что с ним делать. Из-за его широкого использования они решили сохранить API, но намеренно нарушили спецификацию JavaScript. Причина, по которой он реагирует наfalse
при использовании сравнения строгого равенства сundefined
, аtrue
при использовании абстрактного сравнения равенства, связана с умышленным нарушением спецификации, которая явно разрешает это.— «Устаревшие функции — document.all» на WhatWG — спецификация HTML — «Глава 4 — ToBoolean — ложные значения» на YDKJS — Типы и грамматика
Number.MIN_VALUE
— наименьшее число, большее нуля:
Number . MIN_VALUE > 0 ; // -> true
Number.MIN_VALUE
— это5e-324
, т. е. наименьшее положительное число, которое может быть представлено с точностью до числа с плавающей запятой, т. е. оно максимально близко к нулю. Он определяет лучшее разрешение, которое могут дать плавающие числа.Теперь наименьшее значение —
Number.NEGATIVE_INFINITY
, хотя в строгом смысле слова оно не является числовым.— «Почему
0
меньше, чемNumber.MIN_VALUE
в JavaScript?» в StackOverflow
️ Ошибка присутствует в версии 8 версии 5.5 или ниже (Node.js <= 7).️
Все вы знаете, что раздражающая неопределенность не является функцией , а что насчет этого?
// Declare a class which extends null
class Foo extends null { }
// -> [Function: Foo]
new Foo ( ) instanceof null ;
// > TypeError: function is not a function
// > at … … …
Это не часть спецификации. Это просто ошибка, которая уже исправлена, поэтому в будущем с ней проблем быть не должно.
Это продолжение истории предыдущей ошибки в современной среде (проверено на Chrome 71 и Node.js v11.8.0).
class Foo extends null { }
new Foo ( ) instanceof null ;
// > TypeError: Super constructor null of Foo is not a constructor
Это не ошибка, потому что:
Object . getPrototypeOf ( Foo . prototype ) ; // -> null
Если у класса нет конструктора, вызов из цепочки прототипов. Но у родителя нет конструктора. На всякий случай уточню, что null
— это объект:
typeof null === "object" ;
Поэтому от него можно наследовать (хотя в мире ООП за такие условия меня бы побили). Поэтому вы не можете вызвать нулевой конструктор. Если вы измените этот код:
class Foo extends null {
constructor ( ) {
console . log ( "something" ) ;
}
}
Вы видите ошибку:
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
И если вы добавите super
:
class Foo extends null {
constructor ( ) {
console . log ( 111 ) ;
super ( ) ;
}
}
JS выдает ошибку:
TypeError: Super constructor null of Foo is not a constructor
Что если вы попытаетесь добавить два массива?
[ 1 , 2 , 3 ] + [ 4 , 5 , 6 ] ; // -> '1,2,34,5,6'
Объединение происходит. Пошагово это выглядит так:
[ 1 , 2 , 3 ] +
[ 4 , 5 , 6 ] [
// call toString()
( 1 , 2 , 3 )
] . toString ( ) +
[ 4 , 5 , 6 ] . toString ( ) ;
// concatenation
"1,2,3" + "4,5,6" ;
// ->
( "1,2,34,5,6" ) ;
Вы создали массив с четырьмя пустыми элементами. Несмотря ни на что, вы получите массив из трех элементов из-за конечных запятых:
let a = [ , , , ] ;
a . length ; // -> 3
a . toString ( ) ; // -> ',,'
Завершающие запятые (иногда называемые «заключительными запятыми») могут быть полезны при добавлении новых элементов, параметров или свойств в код JavaScript. Если вы хотите добавить новое свойство, вы можете просто добавить новую строку, не изменяя предыдущую последнюю строку, если в этой строке уже используется конечная запятая. Это делает различия контроля версий более чистыми, а редактирование кода может быть менее хлопотным.
— Завершающие запятые в MDN
Равенство массивов — это монстр в 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.15 «Абстрактное сравнение равенства» спецификации.
undefined
и Number
Если мы не передадим никаких аргументов в конструктор Number
, мы получим 0
. Значение undefined
присваивается формальным аргументам, когда фактических аргументов нет, поэтому можно ожидать, что Number
без аргументов принимает undefined
в качестве значения своего параметра. Однако когда мы передадим undefined
, мы получим NaN
.
Number ( ) ; // -> 0
Number ( undefined ) ; // -> NaN
Согласно спецификации:
n
будет +0
.n
? ToNumber(value)
.undefined
ToNumber(undefined)
должен возвращать NaN
.Вот соответствующий раздел:
argument
) 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… подожди, что?» в StackOverflow
Не забывайте о восьмеричных числах:
parseInt ( "06"