Оригинал: Denys Dovhan - What the f*ck JavaScript?
Список забавных и сложных примеров работы JavaScript
JavaScript - великий язык. Он имеет простой синтаксис, большую экосистему и, что важнее всего, замечательное сообщество.
В тоже время, все мы знаем, что JS - это язык со множеством любопытных нюансов. Некоторые из них могут быстро превратить нашу повседневную работу в сущий кошмар, другие - могут заставить нас громко смеяться.
Оглавление
- Цель списка
- Используемые обозначения
- Примеры
- [] равно ![]
- true не равно ![], но также не равно []
- true - это false
- baNaNa
- NaN это не NaN
- Это равняется fail
- [] является инстинным (значением), но не true
- null является ложным (значением), но не равен false
- document.all - это объект, но его значением является undefined
- Минимальное значение (Number.MIN_VALUE) больше нуля
- Функция - это не функция
- Сложение массивов
- Замыкающие запятые (Trailing Commas) в массиве
- Сравнение с массивами - это просто ужас
- undefined и number
- parseInt - плохой парень
- Арифметические операции с true и false
- HTML-комментарии являются валидными в JS
- NaN - это (не) число
- [] и null являются объектами
- Чудесным образом увеличивающиеся числа
- Точность результата выражения 0.1 + 0.2
- Усовершенствование чисел
- Сравнение трех чисел
- Забавная математика
- Сложение регулярных выражений
- Строки не являются экземплярами String
- Вызов функции с помощью обратных кавычек
- Call call call
- Свойство constructor
- Объект - это ключ свойства объекта
- Доступ к прототипам с помощью __proto__
- `${{Object}}`
- Деструктуризация с помощью параметров по умолчанию
- Точки и распространение (распаковка)
- Ярлыки (метки)
- Вложенные метки
- Коварный try..catch
- Множественное наследование
- Генератор, вызывающий самого себя
- Класс класса
- Объекты, не поддающиеся преобразованию
- Причуды стрелочных функций
- Стрелочные функции не могут быть конструктором
- arguments и стрелочные функции
- Необычный return
- Цепочка из значений, присваиваемых объекту
- Доступ к свойствам объекта с помощью массивов
- Null и операторы сравнения
- Number.toFixed() показывает разные числа
- Math.max() меньше Math.min
- Сравнение null и 0
- Переопределение переменной
- Стандартное поведение Array.prototype.sort()
Забавы ради - "Забавы ради: история случайной революции", Линус Торвальдс
Основная цель этого списка - предоставить читателю коллекцию сумасшедших примеров работы 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; массив равен false, но не массив также равен false:
true == [] // -> false
true == ![] // -> false
false == [] // -> true
false == ![] // -> true
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
!!"false" == !!"true" // -> true
!!"false" === !!"true" // -> true
Рассмотрим этот пример шаг за шагом:
// true - это 'истина', представленная числом 1, 'true' в строковом формате - это NaN
true == "true" // -> false
false == "false" // -> false
// 'false' не пустая строка, поэтому является истинным значением
!!"false" // -> true
!!"true" // -> true
"b" + "a" + +"a" + "a" // -> 'baNaNa'
Это бородатая шутка из мира JS. Вот оригинал:
"foo" + +"bar" // -> 'fooNaN'
Выражение оценивается как 'foo' + (+'bar'), где 'bar' преобразуется в NaN (not a number, не число).
NaN === NaN // -> false
Спецификация строго определяет логику, лежащую в основе такого поведения:
- Если Type(x) отличается от Type(y), вернуть false.
- Если Type(x) является числом, тогда i. Если x это NaN, вернуть false. ii. Если y это NaN, вернуть false. iii. ...
Вот определение NaN от IEEE:
Существует четыре взаимоисключающих отношения (между операндами): меньше чем, равно, больше чем и не определено (не упорядочено). Последний случай возникает, когда по крайней мере один операнд является NaN. Каждое NaN должно сравниваться неопределенно со всем, включая себя.
- "Каково рациональное объяснение того, почему все сравнения с NaN IEEE754 возвращают false?" - ответ члена комитета IEEE-754 на StackOverflow
Вы не поверите, но...
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]]
// -> 'fail'
Разбив этот массив символов на части, мы можем заметить, что в нем часто повторяется следующий шаблон:
![] + [] // -> false
![] // -> false
Т.е. мы пытаемся прибавить [] к false. Но из-за ряда вызовов внутренних функций (binary + Operator -> ToPrimitive -> [[DefaultValue]]) мы приходим к преобразованию левого операнда к строке:
![] + [].toString() // 'false'
Рассматривая строку как массив, мы можем получить доступ к первому символу этой строки с помощью [0]:
"false"[0] // -> 'f'
Остальное очевидно (решается аналогичным способом), но откуда берется i? i в fail появляется из сформированной строки 'falseundefined' путем извлечения элемента с индексом ['10'].
Массив является истинным значением, но не равен true.
!![] // -> true
[] == true // -> false
Мы рассмотрели это в одном из предыдущих вопросов.
Несмотря на то, что null является ложным значением, он не равен false.
!!null // -> false
null == false // -> false
В тоже время, другие ложные значения, такие как 0 или '' равны false.
0 == false // -> true
'' == false // -> true
Мы рассмотрели это в одном из предыдущих вопросов.
Это часть браузерного 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, состоит в следовании (в нарушение спецификации) соответствующим алгоритмам, которые явно определяют такое поведение.
-
"Устаревшие возможности - document.all" на WhatWG - спецификация HTML
-
"Часть 4 - ToBoolean - Ложные значения" в "Вы не знаете JS" - Типы & Синтаксис (грамматика)
Number.MIN_VALUE - это наименьшее число, которое больше нуля:
Number.MIN_VALUE > 0 // -> true
Number.MIN_VALUE - это 5e-324, наименьшее положительное число с плавающей точкой, которое максимально близко к нулю. Оно предоставляет лучшее решение, которое могут предложить такие числа. Самым маленьким значением является Number.NEGATIVE_INFINITY, хотя оно не является числом в строгом смысле этого слова.
- "Почему 0 меньше чем Number.MIN_VALUE в JS?" на StackOverflow
Это ошибка (баг) в 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")
Вы создаете массив, состоящий из 4 пустых элементов. Несмотря на это, длина такого массива будет равняться 3 из-за замыкающих запятых:
let a = [,,,]
a.length // -> 3
a.toString() // -> ',,'
Замыкающие запятые (также известные как "конечные запятые") могут быть полезны при добавлении новых элементов, параметров или свойств в JS-код. Если вы хотите добавить новое свойство, вы можете просто добавить новую строку без изменения предыдущей (последней), если эта строка уже содержит замыкающую запятую. Это делает различия в контроле версий чище и облегчает редактирование кода.
- "Замыкающие запятые" на MDN
Сравнение с массивами - это сущий кошмар, в чем вы можете убедиться сами:
[] == '' // -> 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 Алгоритм абстрактного сравнения спецификации.
Если мы не передаем аргументов конструктору Number, то получаем 0. Формальным аргументам при отсутствии фактических присваивается значение undefined, поэтому можно ожидать, что значением отсутствующего параметра Number является undefined. Однако, когда мы передаем undefined, возвращается NaN.
Number() // -> 0
Number(undefined) // -> NaN
Согласно спецификации:
- Если при вызове функции не передается аргументов, пусть b будет +0
- Иначе, пусть b будет ? ToNumber(value)
- В случае undefined, ToNumber(value) должно возвращать NaN
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(null, 24) === 23... подождите, что?" на StackOverflow
Не забудьте про восьмеричные числа:
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 +
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-комментарии были предназначены для изящной деградации браузеров, не понимающих тег script. Эти браузеры, например, Netscape 1.x, давно вышли из употребления (впрочем, кто знает). Поэтому больше не имеет смысла комментировать свой код таким способом.
Поскольку Node.js основан на движке V8, HTML-комментарии в Node также поддерживаются. Более того, они являются частью спецификации:
Тип NaN - 'number':
typeof NaN // -> 'number'
Объяснение того, как работают операторы typeof и instanceof:
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.1 Тип "Число"
- IEEE 754 в Википедии
Хорошо известная шутка. Сложение 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
"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-разработчиков.
console.log.call.call.call.call.call.apply(a => a, [1, 2]) // > 2
Осторожно, это может сломать ваш мозг! Попробуйте воспроизвести данный код в своей голове: мы применяем метод call с помощью метода apply. Подробнее читайте здесь:
- 19.2.3.3 Fuction.prototype.call(thisArg, ...args)
- **19.2.3.1 ** Function.prototype.apply(thisArg, agrArray)
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__ примитива, то получим следующее:
(1).__proto__.__proto__.__proto__ // -> null
Так происходит, поскольку когда нечто не имеет прототипа, оно оборачивается в объектную обертку с помощью метода ToObject. Итак, шаг за шагом:
(1)
.__proto__(
// -> [Number: 0]
1
)
.__proto__.__proto__(
// -> {}
1
).__proto__.__proto__.__proto__ // -> null
Каким будет результат?
`${{ 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
Здесь:
- Мы объявили переменную x без значение, поэтому она undefined.
- Затем мы поместили значение x в свойство x объекта.
- После этого мы извлекаем значение x с помощью деструктуризации и хотим присвоить это значение y. Если значение не определено, тогда мы иcпользуем 1 в качестве значения по умолчанию.
- Инициализация объектов на MDN
Интересные результаты могут быть получены в результате распаковки массивов. Например:
[...[..."..."]].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
Мы рассмотрели это в предыдущем вопросе.
Что вернет выражение? 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, так что такое использование стрелочных функций не имеет особого смысла.
Пример:
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' }
- Стрелочные функции на MDN
Оператор 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 > 0 // -> false
null == 0 // -> false
null >= 0 // -> true
Длинная история короткими словами: если null меньше 0, возвращается false, а null >= 0 возвращает true. Подробное объяснение смотрите здесь.
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.min(1, 4, 7, 2) // -> 1
Math.max(1, 4, 7, 2) // -> 7
Math.min() // -> Infinity
Math.max() // -> -Infinity
Math.min() > Math.max() // -> true
- "Почему Math.max() меньше Math.min()" - статья Charlie Harvey
Это похоже на противоречие:
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 это работает из-за поднятия переменных.
Представьте, что вам нужно отсортировать массив чисел.
[10, 1, 3].sort() // -> [1, 10, 3]
Стандартный алгоритм сортировки основан на приведении элементов к строке и сравнении кодовых обозначений этих строк (в формате UTF-16).
Для правильной сортировки "не строк" необходимо передать sort в качестве аргумента функцию для сравнения (comparefn):
[10, 1, 3].sort((a, b) => a - b) // -> [1, 3, 10]
Алгоритм сортировки в разных браузерах был реализован по-разному. Это приводило к разным результатам. Стандарт ECMAScript 2019 представил алгоритм стабильной сортировки.