Этот пост — список забавных и хитрых примеров на JavaScript. Это отличный язык. У него простой синтаксис, большая экосистема и, что гораздо важнее, огромное сообщество.
В то же время мы все знаем, что JavaScript довольно забавный язык, в котором есть хитрые вещи. Некоторые из них быстро превращают нашу повседневную работу в ад, а некоторые заставляют хохотать. В этом посте рассмотрим некоторые из них.
Содержание
- Мотивация
- Нотация
- Примеры
- [] эквивалентно ![]
- true это false
- baNaNa
- NaN не NaN
- Это fail
- [] является «истинноватым» (truthy), но не true
- null является «ложноватым» (falsy), но не false
- Минимальное значение больше нуля
- Функция не является функцией
- Сложение массивов
- Висящие запятые в массиве
- Эквивалентность массивов — это чудовище
- undefined и Number
- Плохой parseInt
- Вычисления с помощью true и false`
- HTML-комментарии валидны в JavaScript
- NaN
неявляется числом - [] и null являются объектами
- Магическое увеличение чисел
- Точность 0.1 + 0.2
- Патчим числа
- Сравнение трёх чисел
- Забавная математика
- Сложение регулярных выражений
- Строки не являются экземплярами String
- Вызов функций с «обратными кавычками» (backticks)
- Call call call
- Свойство constructor
- Объект как ключ свойства объекта
- Обращение к прототипам с помощью proto
- ${{Object}}
- Деструктурирование со значениями по умолчанию
- Точки и распределение
- Метки
- Вложенные метки
- Коварный try..catch
- Это множественное наследование?
- Генератор, получающий данные из самого себя
- Класс класса
- Объекты, для которых не действует неявное приведение типа (non-coercible objects)
- Хитрые стрелочные функции
- Хитрое возвращение
- Обращение к свойствам объектов с помощью массивов
- Прочие источники
Мотивация
Ради удовольствия
— “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-комментарий <!--
является валидным комментарием и в JavaScript.
// валидный комментарий
<!-- тоже валидный комментарий
Объяснение:
Впечатлены? Комментарии а-ля HTML должны позволять мягко деградировать (degrade gracefully) тем браузерам, которые не понимают тега <script>
. Такие браузеры, вроде Netscape 1.x, уже редко используются. Так что больше нет особого смысла помещать HTML-комментарии внутрь тегов скриптов.
Поскольку Node.js сделан на движке V8, HTML-комментарии поддерживаются и runtime-средой Node.js. Более того, они являются частью спецификации:
NaN не является числом
Типом NaN
является 'number'
:
typeof NaN // -> 'number'
Объяснение:
Объяснение работы операторов typeof
и instanceof
:
[] и null являются объектами
typeof [] // -> 'object'
typeof null // -> 'object'
// однако
null instanceof Object // false
Объяснение:
Поведение оператора typeof
определено здесь:
Согласно спецификации, оператор typeof
возвращает строковое значение в соответствии с Таблица 35: Результаты оператора typeof
. Для null
, обычных, стандартных экзотических и нестандартных экзотических объектов, которые не реализуют [[Call]]
, возвращается строковое значение "object"
.
Однако с помощью метода toString
вы можете проверить тип объекта.
Object.prototype.toString.call([])
// -> '[object Array]'
Object.prototype.toString.call(new Date)
// -> '[object Date]'
Object.prototype.toString.call(null)
// -> '[object Null]'
Магическое увеличение чисел
999999999999999 // -> 999999999999999
9999999999999999 // -> 10000000000000000
10000000000000000 // -> 10000000000000000
10000000000000000 + 1 // -> 10000000000000000
10000000000000000 + 1.1 // -> 10000000000000002
Объяснение:
Причина в стандарте IEEE 754-2008 для двоичных вычислений с плавающей запятой. При таких величинах выполняется округление до ближайшего чётного числа:
- 6.1.6 Числовой тип
- IEEE 754 на Википедии
Точность вычисления 0.1 + 0.2
Широко известная шутка. Результат сложения 0.1
и 0.2
получается невероятно точным:
0.1 + 0.2 // -> 0.30000000000000004
(0.1 + 0.2) === 0.3 // -> false
Объяснение:
Причина описана в вопросе на StackOverflow ”Сломано вычисление с плавающей запятой?”:
Константы0.2
и0.3
будут аппроксимированы до своих истинных значений. Получается, что ближайшее к0.2
значениеdouble
оказывается больше рационального числа0.2
, но ближайшее к0.3
значениеdouble
меньше рационального числа0.3
. Сумма0.1
и0.2
оказывается больше рационального числа0.3
, и следовательно не соответствует константам в коде.
Проблема так хорошо известна, что даже есть сайт 0.30000000000000004.com. Такое происходит в любом языке, использующем вычисления с плавающей запятой, а не только в JavaScript.
Патчим числа
Вы можете добавить свои методы к объектам-обёрткам вроде Number
или String
.
Number.prototype.isOne = function () {
return Number(this) === 1
}
1.0.isOne() // -> true
1..isOne() // -> true
2.0.isOne() // -> false
(7).isOne() // -> false
Объяснение:
Очевидно, что вы можете расширять объект Number
, как и любой другой объект в JavaScript. Но делать это не рекомендуется, если поведение определяемого метода не является частью спецификации. Вот список свойств Number
:
Сравнение трёх чисел
1 < 2 < 3 // -> true
3 > 2 > 1 // -> false
Объяснение:
Почему это работает таким образом? Проблема кроется в первой части выражения. Работает оно так:
1 < 2 < 3 // 1 < 2 -> true
true < 3 // true -> 1
1 < 3 // -> true
3 > 2 > 1 // 3 > 2 -> true
true > 1 // true -> 1
1 > 1 // -> false
Мы можем это исправить это с помощью оператора «больше или равно»
:
3 > 2 >= 1 // true
Подробнее об операторах сравнения:
Забавная математика
Зачастую результаты математических операций в 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
Объяснение:
Что происходит в первых четырёх примерах? Вот маленькая таблица, которая поможет пронять операцию сложения в JavaScript:
Number + Number -> сложение
Boolean + Number -> сложение
Boolean + Boolean -> сложение
Number + String -> конкатенация
String + Boolean -> конкатенация
String + String -> конкатенация
А что насчёт остальных примеров? Применительно к []
и {}
, до операции сложения неявно вызываются методы ToPrimitive
и ToString
. Подробнее о вычислениях:
Сложение регулярных выражений
Вы знали, что можно складывать такие числа?
// Патч для метода toString
RegExp.prototype.toString = function() {
return this.source
}
/7/ - /5/ // -> 2
Объяснение:
Строки не являются экземплярами String
'str' // -> 'str'
typeof 'str' // -> 'string'
'str' instanceof String // -> false
Объяснение:
Конструктор String
возвращает строковое значение:
typeof String('str') // -> 'string'
String('str') // -> 'str'
String('str') == 'str' // -> true
Попробуем с new
:
new String('str') == 'str' // -> true
typeof new String('str') // -> 'object'
Объект? Что это?
new String('str') // -> [String: 'str']
Подробнее о конструкторе String:
Вызов функций с «обратными кавычками» (backticks)
Давайте объявим функцию, которая журналирует все параметры в консоли:
function f(...args) {
return args
}
Несомненно, вы можете вызвать эту функцию вот так:
f(1, 2, 3) // -> [ 1, 2, 3 ]
Но вы знали, что можете вызвать вообще любую функцию с «обратными кавычками»?
f`true is ${true}, false is ${false}, array is ${[1,2,3]}`
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// -> true,
// -> false,
// -> [ 1, 2, 3 ] ]
Объяснение:
Это вовсе не магия, если вы знакомы с маркированными шаблонными литералами (Tagged template literals). В приведённом примере функция f
является тегом для шаблонного литерала. Теги перед шаблонными литералами позволяют разбирать эти литералы с помощью функции. Первый аргумент функции-тега содержит массив строковых значений. Остальные аргументы связаны с выражениями. Пример:
function template(strings, ...keys) {
// что-то делает со строками и ключами…
}
Это магия, относящаяся к известной библиотеке styled-components, популярной в React-сообществе.
Call call call
Найдено @cramforce
console.log.call.call.call.call.call.apply(a => a, [1, 2])
Объяснение:
Внимание, это может сломать вам мозг! Попробуйте воспроизвести этот код в голове: мы применяем метод call
с помощью метода apply
. Подробнее:
- 19.2.3.3 Function.prototype.call (thisArg, ...args)
- 19.2.3.1 Function.prototype.apply (thisArg, argArray)
Свойство constructor
const c = 'constructor'
c[c][c]('console.log("WTF?")')() // -> WTF?
Объяснение:
Разберём пример пошагово:
// Объявляем новую константу, которая является строковым значением 'constructor'
const c = 'constructor'
// c — это строковое значение
c // -> 'constructor'
// получаем конструктор строки
c[c] // -> [Function: String]
// Получаем конструктор конструктора
c[c][c] // -> [Function: Function]
// Вызываем конструктор Function и передаём тело новой функции в качестве аргумента
c[c][c]('console.log("WTF?")') // -> [Function: anonymous]
// А затем вызываем эту анонимную функцию
// Получаем в консоли строку 'WTF?'
c[c][c]('console.log("WTF?")')() // -> WTF?
Object.prototype.constructor
возвращает ссылку на функцию-конструктор Object
, которая создала объект-экземпляр. В случае со строковыми это String
, в случае с числами это Number
, и так далее.
Объект как ключ свойства объекта
{ [{}]: {} } // -> { '[object Object]': {} }
Объяснение:
Почему это работает таким образом? Здесь мы используем вычисленное имя свойства (Computed property name). При передаче объекта между этими скобками, объект неявно преобразуется в строковое значение, так что мы получаем ключ свойства '[object Object]'
и значение {}
.
Можно сделать «скобочный ад»:
({[{}]:{[{}]:{}}})[{}][{}] // -> {}
// structure:
// {
// '[object Object]': {
// '[object Object]': {}
// }
// }
Подробнее об объектных литералах:
Доступ к прототипам с помощью proto
Как мы знаем, примитивы не имеют прототипов. Однако если мы попытаемся получить для них примитивов значение __proto__
, то получим:
(1).__proto__.__proto__.__proto__ // -> null
Объяснение:
Когда что-то не имеет прототипа, оно обёртывается в объект-обёртку с помощью метода ToObject
. Пошагово:
(1).__proto__ // -> [Number: 0]
(1).__proto__.__proto__ // -> {}
(1).__proto__.__proto__.__proto__ // -> null
Подробнее о __proto__
:
${{Object}}
Какой будет результат этого выражения?
`${{Object}}`
Ответ:
// -> '[object Object]'
Объяснение:
Мы определили объект со свойством Object
с помощью нотации сокращённых свойств (Shorthand property notation):
{ Object: Object }
Затем мы передали этот объект в шаблонный литерал, так что его вызывает метод toString
. Поэтому мы получаем строку '[object Object]'
.
Деструктурирование со значениями по умолчанию
Рассмотрим пример:
let x, { x: y = 1 } = { x }; y;
Это прекрасная задача для собеседований. Каким будет значение y
? Ответ:
// -> 1
Объяснение:
let x, { x: y = 1 } = { x }; y;
// ↑ ↑ ↑ ↑
// 1 3 2 4
- Мы объявили
x
без значения, поэтому оноundefined
. - Затем упаковали свойство
x
свойство объектаx
. - Затем с помощью деструктурирования извлекли значение
x
, и хотим присвоить егоy
. Если значение не определено, то мы используем1
в качестве значения по умолчанию. - Возвращаем значение
y
.
Точки и разбиение (spreading)
Можно привести интересный пример с распределением массивов.
[...[...'...']].length // -> 3
Объяснение:
Почему 3
? Когда мы используем оператор разбиения TODO, то вызывается метод @@iterator
, а для получения значений, которые нужно итерировать, используется возвращённый итератор. Итератор для строк по умолчанию разбивает строку на символы, которые затем упаковываются в массив. Затем мы разбиваем этот массив и снова упаковываем в массив.
Строка '...'
состоит из трёх символов .
, так что длина получившегося массива равна 3
.
Пошагово:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3
Очевидно, что мы можем разбить и обернуть элементы массива сколько угодно раз:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...[...'...']]] // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]] // -> [ '.', '.', '.' ]
// и так далее…
Метки
Не так много программистов знают о метках в JavaScript. Они весьма интересны:
foo: {
console.log('first');
break foo;
console.log('second');
}
// -> first
// -> undefined
Объяснение:
Маркированные выражения используются с выражениями break
или continue
. С помощью меток вы можете пометить цикл, а затем использовать выражения break
или continue
чтобы показать, должна ли программа прервать цикл или продолжить его выполнение.
В приведённом примере мы помечаем как foo
. Затем выполняем console.log('first');
и происходит прерывание выполнения.
Подробнее о метках в JavaScript:
Вложенные метки
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5
Объяснение:
Как в предыдущем примере, читайте:
Коварный try..catch
Что возвращает это выражение? 2
или 3
?
(() => {
try {
return 2;
} finally {
return 3;
}
})()
Ответ 3
. Удивлены?
Объяснение:
Это множественное наследование?
Взгляните на этот пример:
new (class F extends (String, Array) { }) // -> F []
Это множественное наследование? Нет.
Объяснение:
Самое интересное связано со значением выражения extends ((String, Array))
. Оператор группирования всегда возвращает последний аргумент, так что (String, Array)
по сути просто Array
. То есть мы создали класс, который расширяет Array
.
Генератор, получающий данные из самого себя
Рассмотрим пример:
(function* f() { yield f })().next()
// -> { value: [GeneratorFunction: f], done: false }
Как видите, возвращаемое значение является объектом, у которого value
эквивалентно f
. В этом случае мы можем сделать так:
(function* f() { yield f })().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }
// и опять
(function* f() { yield f })().next().value().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }
// и опять
(function* f() { yield f })().next().value().next().value().next().value().next()
// -> { value: [GeneratorFunction: f], done: false }
// и так далее
// …
Объяснение:
Чтобы понять, почему это работает таким образом, читайте спецификацию:
Класс класса
Рассмотрим этот сбивающий с толку синтаксис:
(typeof (new (class { class () {} }))) // -> 'object'
Похоже, мы объявляем класс внутри класса. Должна быть ошибка, однако мы получаем строку 'object'
.
Объяснение:
Со времён ECMAScript 5 ключевые слова могут использоваться в качестве наименований свойств. Так что это сродни простому примеру с объектом:
const foo = {
class: function() {}
};
А ES6 стандартизировал сокращённые определения методов. К тому же классы могут быть анонимными. Так что если отбросить часть: function
, то получим:
class {
class() {}
}
Результатом класса по умолчанию всегда будет простой объект. И его typeof
должен возвращать 'object'
.
Почитать подробнее:
Объекты, для которых не действует неявное приведение типа (Non-coercible objects)
С помощью Well-Known Symbols можно избавиться от неявного приведения типа:
function nonCoercible(val) {
if (val == null) {
throw TypeError('nonCoercible should not be called with null or undefined')
}
const res = Object(val)
res[Symbol.toPrimitive] = () => {
throw TypeError('Trying to coerce non-coercible object')
}
return res
}
Это можно рассматривать так:
// объекты
const foo = nonCoercible({foo: 'foo'})
foo * 10 // -> TypeError: Trying to coerce non-coercible object
foo + 'evil' // -> TypeError: Trying to coerce non-coercible object
// строковые значения
const bar = nonCoercible('bar')
bar + '1' // -> TypeError: Trying to coerce non-coercible object
bar.toString() + 1 // -> bar1
bar === 'bar' // -> false
bar.toString() === 'bar' // -> true
bar == 'bar' // -> TypeError: Trying to coerce non-coercible object
// числа
const baz = nonCoercible(1)
baz == 1 // -> TypeError: Trying to coerce non-coercible object
baz === 1 // -> false
baz.valueOf() === 1 // -> true
Объяснение:
Хитрые стрелочные функции
Рассмотрим пример:
let f = () => 10
f() // -> 10
Так, отлично, а что насчёт этого:
let f = () => {}
f() // -> undefined
Объяснение:
Вы можете ожидать получить {}
вместо undefined
. Причина в том, что фигурные скобки являются частью синтаксиса стрелочных функций, так что f
будет возвращено неопределённым.
Хитрое возвращение
Пример:
(function () {
return
{
b : 10
}
})() // -> undefined
Объяснение:
return
и возвращаемое выражение должны находиться в одной строке:
(function () {
return {
b : 10
}
})() // -> { b: 10 }
Обращение к свойствам объектов с помощью массивов
var obj = { property: 1 }
var array = ['property']
obj[array] // -> 1
Что насчёт псевдомногомерных массивов?
var map = {}
var x = 1
var y = 2
var z = 3
map[[x, y, z]] = true
map[[x + 10, y, z]] = true
map["1,2,3"] // -> true
map["11,2,3"] // -> true
Объяснение:
Оператор []
преобразует переданное выражение toString
. Преобразование одноэлементного массива в строку — это преобразование элемента в строку:
['property'].toString() // -> 'property'`
Другие источники
- wtfjs.com — коллекция особенных нарушений, несоответствий и просто крайне неочевидных моментов.
- Wat — прекрасное выступление Гэри Бернхардта на CodeMash 2012.
- What the… JavaScript? — попытка Кайла Симпсона «убрать безумие» из JavaScript. Он хочет помочь вам писать более чистый, элегантный, читабельный код, а также вдохновить людей вносить свой вклад в open source-движение.