Skip to content

Latest commit

 

History

History
1870 lines (1352 loc) · 82.6 KB

wtfjs.md

File metadata and controls

1870 lines (1352 loc) · 82.6 KB

Что за черт, JavaScript?

На главную

Оригинал: Denys Dovhan - What the f*ck JavaScript?

Список забавных и сложных примеров работы JavaScript

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

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

Оглавление

Цель списка

Забавы ради - "Забавы ради: история случайной революции", Линус Торвальдс

Основная цель этого списка - предоставить читателю коллекцию сумасшедших примеров работы JS и объяснить, почему так происходит... там, где это возможно. Всегда интересно изучать нечто, чего мы раньше не знали, не правда ли?

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

Если вы профессиональный разработчик, можете считать эти примеры собранием всех причуд и неожиданных поворотов любимого нами JS.

В любом случае, прочитайте это. Вероятно, вы откроете для себя нечто новое.

Используемые обозначения

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

1 + 1 // -> 2

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

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

// - это просто комментарий. Например:

// присваиваем константе foo функцию в качестве значения
const foo = function () {}

Примеры

[] равно ![]

Массив равен не массиву:

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

Объяснение:

Оператор нестрогого (абстрактного) равенства с целью сравнения операндов приводит их к числу, оба операнда становятся числом 0 по разным причинам. Массивы являются инстинными значениями, поэтому значением правого операнда после инверсии является false, которое затем приводится к 0. Несмотря на то, что левый операнд, пустой массив, является истинным значением, он также приводится к 0.

Это выглядит так:

+[] == +![]
0 == +false
0 == 0
true

true не равно ![], но также не равно []

Массив не равен true, но не массив также не равен true; массив равен false, но не массив также равен false:

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

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

Объяcнение:

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

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

true == [] // -> false

toNumber(true) // -> 1
toNumber([])   // -> 0

1 == 0 // -> false

true == ![] // -> false

![] // -> false

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

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

false == [] // -> true

toNumber(false) // -> 0
toNumber([])    // -> 0

0 == 0 // -> true

false == ![] // -> true

![] // -> false

false == false

true - это false

!!"false" == !!"true"  // -> true
!!"false" === !!"true" // -> true

Объяснение:

Рассмотрим этот пример шаг за шагом:

// true - это 'истина', представленная числом 1, 'true' в строковом формате - это NaN
true == "true"   // -> false
false == "false" // -> false

// 'false' не пустая строка, поэтому является истинным значением
!!"false" // -> true
!!"true"  // -> true

baNaNa

"b" + "a" + +"a" + "a" // -> 'baNaNa'

Это бородатая шутка из мира JS. Вот оригинал:

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

Объяснение:

Выражение оценивается как 'foo' + (+'bar'), где 'bar' преобразуется в NaN (not a number, не число).

NaN это не NaN

NaN === NaN // -> false

Объяснение:

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

  1. Если Type(x) отличается от Type(y), вернуть false.
  2. Если Type(x) является числом, тогда i. Если x это NaN, вернуть false. ii. Если y это NaN, вернуть false. iii. ...

Вот определение NaN от IEEE:

Существует четыре взаимоисключающих отношения (между операндами): меньше чем, равно, больше чем и не определено (не упорядочено). Последний случай возникает, когда по крайней мере один операнд является NaN. Каждое NaN должно сравниваться неопределенно со всем, включая себя.

Это равняется fail

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

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

Объяснение:

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

![] + [] // -> false
![]      // -> false

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

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

Рассматривая строку как массив, мы можем получить доступ к первому символу этой строки с помощью [0]:

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

Остальное очевидно (решается аналогичным способом), но откуда берется i? i в fail появляется из сформированной строки 'falseundefined' путем извлечения элемента с индексом ['10'].

[] является инстинным (значением), но не true

Массив является истинным значением, но не равен true.

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

Объяснение:

Мы рассмотрели это в одном из предыдущих вопросов.

null является ложным (значением), но не равен false

Несмотря на то, что null является ложным значением, он не равен false.

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

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

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

Объяснение:

Мы рассмотрели это в одном из предыдущих вопросов.

document.all - это объект, но его значением является undefined

Это часть браузерного 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-коде. Когда в стандарте появился новый интерфейс (такой как document.getElementById), рассматриваемый API был признан устаревшим, и комитету по стандартизации пришлось решать, что с ним делать. По причине широкого использования было принято решение о его сохранении. Таким образом, комитет умышленно пошел на нарушение JS-спецификации. Причина, по которой результатом строгого сравнения этого API с undefined является false, а результатом абстрактного сравнения - true, состоит в следовании (в нарушение спецификации) соответствующим алгоритмам, которые явно определяют такое поведение.

Минимальное значение (Number.MIN_VALUE) больше нуля

Number.MIN_VALUE - это наименьшее число, которое больше нуля:

Number.MIN_VALUE > 0 // -> true

Объяснение:

Number.MIN_VALUE - это 5e-324, наименьшее положительное число с плавающей точкой, которое максимально близко к нулю. Оно предоставляет лучшее решение, которое могут предложить такие числа. Самым маленьким значением является 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: function is not a function
// > 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")

Замыкающие запятые (Trailing Commas) в массиве

Вы создаете массив, состоящий из 4 пустых элементов. Несмотря на это, длина такого массива будет равняться 3 из-за замыкающих запятых:

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

Объяснение:

Замыкающие запятые (также известные как "конечные запятые") могут быть полезны при добавлении новых элементов, параметров или свойств в 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. Если при вызове функции не передается аргументов, пусть b будет +0
  2. Иначе, пусть b будет ? ToNumber(value)
  3. В случае undefined, ToNumber(value) должно возвращать 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

Объяснение:

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

Не забудьте про восьмеричные числа:

parseInt("06") // 6
parseInt("08") // 8, если поддерживается ECMAScript 5
parseInt("08") // 0, если не поддерживается

Объяснение:

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

parseInt всегда приводит значение к строке:

parseInt({ toString: () => 2, valueOf: () => 1 }) // -> 2
Number({ toString: () => 2, valueOf: () => 1 })   // -> 1

Будьте внимательны при преобразовании чисел с плавающей точкой:

parseInt(0.000001)    // -> 0
parseInt(0.0000001)   // -> 1
parseInt(1 / 1999999) // -> 5

Объяснение:

parseInt принимает строку в качестве аргумента и возвращает целое число в определенной системе счисления. parseInt также исключает (игнорирует) все, что следует за первым "не числом" в строке, включая сам недопустимый символ. 0.000001 приводится к строке "0.000001" и parseInt возвращает 0. 0.0000001 приводится к строке, которая имеет такое представление: "1e-7", поэтому parseInt возвращает 1. 1 / 1999999 интерпретируется как "5.00000250000125e-7", parseInt возвращает 5.

Арифметические операции с 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. Согласно спецификации этот метод возвращает:

Если <span>argument</span> является true, вернуть 1. Если <span>argument</span> является false, вернуть +0.

Вот почему мы можем использовать логические значения как числа и получать правильные результаты.

HTML-комментарии являются валидными в JS

Вы будете удивлены, но <!-- --> (что известно как HTML-комментарий) является валидным в JS.

// валидный комментарий
<!-- тоже валидный комментарий -->

Объяснение:

Удивлены? HTML-комментарии были предназначены для изящной деградации браузеров, не понимающих тег script. Эти браузеры, например, Netscape 1.x, давно вышли из употребления (впрочем, кто знает). Поэтому больше не имеет смысла комментировать свой код таким способом.

Поскольку Node.js основан на движке V8, HTML-комментарии в Node также поддерживаются. Более того, они являются частью спецификации:

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 для двоичных чисел с плавающей точкой. В данном случае имеет место округление до ближайшего четного числа. Подробнее читайте здесь:

Точность результата выражения 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 в вашей программе всегда являются приближенными значениями к их истинным значениям. Так уж вышло, что double для 0.2 больше, чем рациональное число 0.2, а double для 0.3 меньше, чем рациональное число 0.3. Сумма 0.1 и 0.2 получается больше рационального числа 0.3 и не совпадает с константой в вашем коде.

Эта проблема настолько широко известна, что в ее честь даже назвали веб-сайт 0.30000000000000004.com. Данная проблема встречается во всех языках (программирования), использующих математику для чисел с плавающей точкой, а не только в JS.

Усовершенствование чисел

Вы можете добавлять собственные методы к объектным оберткам, таким как 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 как любой JS-объект. Однако делать этого не рекомендуется, если поведение определенного метода не является частью спецификации. Вот список свойств 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

Подробнее об операторах сравнения читайте в спецификации:

Забавная математика

Часто результаты арифметических операций в JS могут приводить в замешательство. Изучите следующие примеры:

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

Объяснение:

Что происходит в первых четырех случаях? Вот небольшая таблица для понимания сложения в JS:

Number  + Number  -> сложение
Boolean + Number  -> сложение
Boolean + Boolean -> сложение
Number  + String  -> конкатенация
String  + Boolean -> конкатенация
String  + String  -> конкатенация

Что насчет других примеров? Для [] и {} при сложении неявно вызываются методы ToPrimitive и ToString. Подробнее о процессе оценивания выражений читайте в спецификации:

Обратите внимание, что {} + [] является исключением. Причина, по которой данное выражение отличается от [] + {} состоит в том, что {} интерпретируется как блок кода, а +[] приводится к числу. Это выглядит так:

{
  // блок кода
}
+[] // -> 0

Для того, чтобы получить результат, аналогичный [] + {}, выражение следует обернуть в круглые скобки:

({} + []) // -> [object Object]

Сложение регулярных выражений

Знаете ли вы, что можете складывать числа таким способом:

// расширяем метод 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 читайте в спецификации:

Вызов функции с помощью обратных кавычек

Объявим функцию, которая выводит в консоль все параметры:

function f(...args) {
  return args
}

Без сомнения, вы знаете, что такая функция вызывается следующим образом:

f(1, 2, 3) // -> [1, 2, 3]

Но знаете ли вы, что можете вызвать любую функцию с помощью обратных кавычек?

f`true is ${true}, false if ${false}, array is ${[1, 2, 3]}`
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// ->   true,
// ->   false,
// ->   [ 1, 2, 3 ] ]

Объяснение:

Здесь нет никакого вошлебства, если вы знакомы с тегированными шаблонными литералами. В приведенном примере функция f является тегом для шаблонного литерала. Теги перед шаблонными литералами позволяют разбирать (парсить) шаблонные литералы с помощью функции обратного вызова. Первый аргумент тегированной функции представляет собой массив строковых значений. Остальные аргументы зависят от выражения. Например:

function template(strings, ...keys) {
  // обрабатываем string (строки) и keys (ключи)
}

Это лежит в основе знаменитой библиотеки styled-components, широко известной в сообществе React-разработчиков.

Call call call

console.log.call.call.call.call.call.apply(a => a, [1, 2]) // > 2

Объяснение:

Осторожно, это может сломать ваш мозг! Попробуйте воспроизвести данный код в своей голове: мы применяем метод call с помощью метода apply. Подробнее читайте здесь:

Свойство 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]': {} }

Объяснение:

Почему так происходит? Здесь мы используем имя вычисляемого свойства. Когда вы передаете объект между скобками, он приводится к строке, получаем ключ свойства '[object Object]' и значение { }.

Как насчет ада из скобок:

({ [{}]: { [{}]: {} } }[{}][{}]) // -> {}

// структура:
// {
//   '[object Object]': {
//     '[object Object]': {}
//   }
// }

Доступ к прототипам с помощью proto

Как все мы знаем, примитивы не имеют прототипов. Однако, если мы попытаемся получить значение __proto__ примитива, то получим следующее:

(1).__proto__.__proto__.__proto__ // -> null

Объяснение:

Так происходит, поскольку когда нечто не имеет прототипа, оно оборачивается в объектную обертку с помощью метода ToObject. Итак, шаг за шагом:

(1)
  .__proto__(
    // -> [Number: 0]
    1
  )
  .__proto__.__proto__(
    // -> {}
    1
  ).__proto__.__proto__.__proto__ // -> null

${{Object}}

Каким будет результат?

`${{ Object }}`

Ответ:

// -> '[object Object]'

Объяснение:

Мы определяем объект со свойством Object, используя сокращенное обозначение свойства:

{
  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

Здесь:

  1. Мы объявили переменную x без значение, поэтому она undefined.
  2. Затем мы поместили значение x в свойство x объекта.
  3. После этого мы извлекаем значение x с помощью деструктуризации и хотим присвоить это значение y. Если значение не определено, тогда мы иcпользуем 1 в качестве значения по умолчанию.

Точки и распространение (распаковка)

Интересные результаты могут быть получены в результате распаковки массивов. Например:

[...[..."..."]].length // -> 3

Объяснение:

Почему 3? При использовании оператора распространения (...) вызывается метод @@iterator, данный итератор используется для получения перебираемых значений. Стандартный итератор для строки разбивает ее на отдельные символы. После распаковки мы помещаем эти символы в массив. Затем мы снова распаковываем массив и заворачиваем (упаковываем) его элементы в новый массив.

Строка '...' состоит из трех символов .. Поэтому длина результирующего массива равна 3.

Вот как это выглядит:

[...'...']             // -> [ '.', '.', '.' ]
[...[...'...']]        // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3

Мы можем распаковывать и упаковывать (заворачивать) элементы массива сколько угодно, результат будет одинаковым:

[...'...']                 // -> [ '.', '.', '.' ]
[...[...'...']]            // -> [ '.', '.', '.' ]
[...[...[...'...']]]       // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]]  // -> [ '.', '.', '.' ]
// и т.д.

Ярлыки (метки)

Немногие программисты знают о ярлыках (метках) в JS. Они представляют некоторый интерес:

foo: {
    console.log("first")
    break foo
    console.log("second")
}

// > first
// -> undefined

Объяснение:

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

В приведенном примере мы указали ярлык foo. Вызывается console.log("first") и выполнение кода прекращается.

Вложенные метки

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). Оператор группировки всегда возвращает последний аргумент, так что на самом деле мы наследуемся только от Array. Это означает, что мы создаем новый класс, расширяющий Array.

Генератор, вызывающий самого себя

Изучите следующий пример, в котором генератор вызывает (yield) самого себя (выводит собственное значение):

(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'.

Объяснение:

Начиная с ECMAScript5, ключевые слова можно использовать как названия свойств объектов. Думайте об этом так:

const foo = {
  class: function () {}
}

ES6 стандартизировал сокращенные обозначения методов. Кроме того, классы могут быть анонимными. Так что если мы опустим : function, то получим следующее:

class {
  class () {}
}

Результатом стандартного класса всегда является простой объект. Поэтому его типом является 'object'.

Объекты, не поддающиеся преобразованию

С помощью хорошо известных символов (Well-Known Symbols) можно избавиться от принудительного приведения типов. Взгляните на пример:

function nonCoercible(val) {
  if (val == null) {
    throw TypeError("nonCoercible не должна вызываться с null или undefined")
  }

  const res = Object(val)

  res[Symbol.toPrimitive] = () => {
    throw TypeError("Вы пытаетесь преобразовать непреобразуемый объект");
  }

  return res
}

Мы можем использовать это следующим образом:

// объекты
const foo = nonCoercible({ foo: "foo" })

foo * 10 // -> TypeError: Вы пытаетесь преобразовать непреобразуемый объект
foo + "evil" // -> TypeError: Вы пытаетесь преобразовать непреобразуемый объект

// строки
const bar = nonCoercible("bar")

bar + "1" // -> TypeError: Вы пытаетесь преобразовать непреобразуемый объект
bar.toString() + 1 // -> bar1
bar === "bar" // -> false
bar.toString() === "bar" // -> true
bar == "bar" // -> TypeError: Вы пытаетесь преобразовать непреобразуемый объект

// числа
const baz = nonCoercible(1)

baz == 1 // -> TypeError: Вы пытаетесь преобразовать непреобразуемый объект
baz === 1 // -> false
baz.valueOf() === 1 // -> true

Объяснение:

Причуды стрелочных функций

Пример:

let f = () => 10
f() // -> 10

Хорошо, но что насчет этого:

let f = () => {}
f() // -> undefined

Объяснение:

Мы ожидаем получить { }, а не undefined. Фигурные скобки являются частью синтаксиса стрелочных функций, поэтому f возвращает undefined. Однако вернуть объект из стрелочной функции все же возможно, обернув возвращаемое значение круглыми скобками:

let f = () => ({})
f()  // -> {}

Стрелочные функции не могут быть конструктором

Пример:

let f = function () {
  this.a = 1
}
new f() // -> { 'a': 1 }

Попробуем сделать тоже самое со стрелочной функцией:

let f = () => {
  this.a = 1
}
new f() // -> TypeError: f is not a constructor (f не является конструктором)

Объяснение:

Стрелочные функции не могут использоваться как конструкторы и выбрасывают ошибку при использовании с ключевым словом new. Их this берется из лексического окружения, и у них отсутствует свойство prototype, так что такое использование стрелочных функций не имеет особого смысла.

arguments и стрелочные функции

Пример:

let f = function () {
  return arguments
}
f("a") // -> { '0': 'a' }

Попробуем сделать тоже самое со стрелочной функцией:

let f = () => arguments
f("a") // -> Uncaught ReferenceError: arguments is not defined (arguments не определена)

Объяснение:

Стрелочные функции являются сокращенным вариантом обычных функций, они короткие и имеют лексическое this. В тоже время, стрелочные функции не могут быть привязаны (bind) к объекту arguments. В качестве альтернативы можно использовать остальные (оставшиеся) параметры (rest parameters) для получения аналогичных результатов:

let f = (...args) => args
f("a") // -> { '0': 'a' }

Необычный return

Оператор return также имеет свои причуды. Например:

(function() {
  return
  {
      b: 10;
  }
})() // -> undefined

Объяснение:

return и возвращаемое значение должны находиться на одной линии (строке):

(function() {
  return { b: 10 }
})() // -> { b: 10 }

Это объясняется концепцией под названием "автоматическая вставка точки с запятой", которая автоматически вставляет точки запятой почти после каждой новой линии (строки). В первом примере точка с запятой вставляется между return и объектным литералом, поэтому функция возвращает undefined, а сам объектный литерал не оценивается.

Цепочка из значений, присваиваемых объекту

const foo = { n: 1 }
const bar = foo

foo.x = foo = { n: 2 }

foo.x // -> undefined
foo   // -> { n: 2 }
bar   // -> { n: 1, x: { n: 2 } }

Справа налево, {n: 2} присваивается foo, результат присваивается foo.x, так почему bar равняется {n: 1, x: {n: 2}}, когда bar ссылается на foo. Почему значением foo.x является undefined, а значением bar.x является {n: 2}?

Объяснение:

Foo и bar ссылаются на один объект, {n: 1}, значения разрешаются до присваивания. foo = {n: 2} создает новый объект, после чего foo ссылается на этот объект. Дело в том, что foo в foo.x = ... все еще ссылается на старый foo = {n: 1} и обновляет его, добавляя свойство x. После цепочки присвоений bar по-прежнему ссылается на старый объект foo, а foo ссылается на новый объект {n: 2}, который не имеет свойства x.

Это выглядит так:

const foo = { n: 1 }
const bar = foo

foo = { n: 2 } // -> {n: 2}
bar.x = foo    // -> {n: 1, x: {n: 2}}
// bar.x указывает на новый объект foo
// это не является эквивалентом bar.x = {n: 2}

Доступ к свойствам объекта с помощью массивов

const obj = { property: 1 }
const arr = ['property']

obj[arr] // -> 1

Что насчет псевдомногомерных массивов?

const map = {}
const x = 1
const y = 2
const 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'
[1, 2, 3].toString()    // -> '1,2,3'

Null и операторы сравнения

null > 0  // -> false
null == 0 // -> false

null >= 0 // -> true

Объяснение:

Длинная история короткими словами: если null меньше 0, возвращается false, а null >= 0 возвращает true. Подробное объяснение смотрите здесь.

Number.toFixed() показывает разные числа

Number.toFixed() может вести себя по-разному в разных браузерах. Например:

(0.7875).toFixed(3)
// Firefox: -> 0.787
// Chrome: -> 0.787
// IE11: -> 0.788
(0.7876).toFixed(3)
// Firefox: -> 0.788
// Chrome: -> 0.788
// IE11: -> 0.788

Объяснение:

На первый взгляд может показаться, что IE11 ведет себя правильно, а Firefox/Chrome ошибаются, но в действительности Firefox/Chrome более строго следуют стандартам для чисел с плавающей точкой (IEEE-754), а IE11 немного их нарушает, вероятно, с целью предоставлния более точных результатов.

Понять, почему так происходит, можно с помощью нескольких простых тестов:

// подтверждаем странный результат округления 5 в меньшую сторону
(0.7875).toFixed(3) // -> 0.787
// это выглядит как 5 при расширении числа
// до пределов 64-битных чисел двойной точности
(0.7875).toFixed(14) // -> 0.78750000000000
// но что если выйти за эти пределы?
(0.7875).toFixed(20) // -> 0.78749999999999997780

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

В данном случае "5", в конечном счете, является очень близким к действительному значению 5. Округление "5" до разумной длины будет округлять ее как 5... но на самом деле это не совсем 5.

IE11 даже в случае toFixed(20) после 5 показывает только нули. Кажется, он прибегает к принудительному округлению во избежание проблем, связанных с аппаратными органичениями.

Math.max() меньше Math.min

Math.min(1, 4, 7, 2) // -> 1
Math.max(1, 4, 7, 2) // -> 7
Math.min() // -> Infinity
Math.max() // -> -Infinity
Math.min() > Math.max() // -> true

Объяснение:

Сравнение null и 0

Это похоже на противоречие:

null == 0 // -> false
null > 0  // -> false
null >= 0 // -> true

Как может 0 быть больше null, если null >= 0 возвращает true?

Объяснение:

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

Первым выражением является абстрактное сравнение null == 0. Обычно, если значения нельзя сравнить напрямую, они приводятся к числу. Поэтому логично ожидать следующего:

// это не то, что происходит на самом деле
(null == 0 + null) == +0
0 == 0
true

Однако согласно спецификации принудительное приведение типов не применяется к null и undefined. Если одним из операндов равенства является null, вторым операндом должен быть null или undefined, только в этом случае выражение вернет true. В данном случае это не так, поэтому возвращается false.

Вторым выражением является null > 0. В данном случае алгоритм отличается от абстрактного равентсва и приводит null к числу. Получаем следующее:

null > 0
+null > +0
0 > 0
false

Наконец, третьим выражением является null >= 0. Можно подумать, что данное выражение эквивалентно null > 0 || null == 0; если бы это было правдой, то выражение вернуло бы false. Однако оператор >= работает совсем по-другому, его алгоритм идентичен алгоритму работы оператора <. Получаем следующее:

null >= 0
!(null < 0)
!(+null < +0)
!(0 < 0)
!false
true

Переопределение переменной

JS допускает переопределение переменных:

a = 1
a = 2
// это также является валидным
a = 3, a = 4
a // -> 4

Это возможно даже в строгом режиме:

'use strict'
var a = 1, a = 2, a = 3
var a = 4
var a = 5
a // -> 5

Объяснение:

Все определения объединяются в одно.

Если мы объявим переменную с помощью ключевего слова let или const, то при попытке переопределить такую переменную будет выброшено исключение SyntaxError: Identifier 'a' has already been declared (переменная 'a' уже определена). С var это работает из-за поднятия переменных.

Стандартное поведение Array.prototype.sort()

Представьте, что вам нужно отсортировать массив чисел.

[10, 1, 3].sort() // -> [1, 10, 3]

Объяснение:

Стандартный алгоритм сортировки основан на приведении элементов к строке и сравнении кодовых обозначений этих строк (в формате UTF-16).

Для правильной сортировки "не строк" необходимо передать sort в качестве аргумента функцию для сравнения (comparefn):

[10, 1, 3].sort((a, b) => a - b) // -> [1, 3, 10]

Алгоритм сортировки в разных браузерах был реализован по-разному. Это приводило к разным результатам. Стандарт ECMAScript 2019 представил алгоритм стабильной сортировки.