JavaScript спецификация: темные стороны
JavaScript спецификация называется ECMAScript. Язык популярен и обладает низким порогом входа. Но есть моменты, которые требуют отдельных объяснений.
Мы не будем заострять внимание на востребованности JS. Чтобы понять, о чем пойдет речь, предлагаем ознакомиться со статьями «JavaScript и то, что вы о нем не знали», а также «Перлы языка JavaScript».
Добавление RegExps
Знаете ли вы, что можете добавлять такие цифры?
// устанавливаем метод toString RegExp.prototype.toString = function () { Return this.source } / 7 / - / 5 / // -> 2
Объяснение:
- JavaScript спецификация 21.2.5.10. Получить RegExp.prototype.source
Функции вызова
Давайте объявим функцию, которая выводит все параметры в консоль:
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 ] ]
Объяснение:
Это вовсе не волшебство. В приведенном выше примере функция f является тегом для шаблонного литерала. Первый аргумент содержит массив строковых значений. Остальные аргументы связаны с выражениями. Пример:
function template(strings, ...keys) { // делаем что-то со строками и ключами… }
Это волшебство знаменитой библиотеки под названием styled-components, столь популярной в сообществе React.
Ссылка на спецификацию:
- JavaScript спецификация 12.3.7 Tagged Templates
Вызов вызов вызов
Найдено у @cramforce.
console.log.call.call.call.call.call.apply(a => a, [1, 2])
Объяснение:
Внимание, это может сломать вам голову! Попробуйте мысленно воспроизвести этот код: мы применяем метод call с помощью метода apply. Узнайте больше:
- JavaScript спецификация 19.2.3.3 Функция.prototype.call (thisArg, ... args)
- ** 19.2.3.1 ** Функция.prototype.apply (thisArg, argArray)
JavaScript спецификация: свойство конструктора
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.prototype.constructor на MDN
- JavaScript спецификация 19.1.3.1 Object.prototype.constructor
Объект как ключ к свойствам объекта
{ [{}]: {} } // -> { '[object Object]': {} }
Объяснение:
Почему это так работает? Здесь мы используем вычисленное имя объекта. Когда вы передаете объект между этими скобками, получаете ключ свойства '[object Object]' и значение {}.
Мы можем сделать следующим образом:
({[{}]:{[{}]:{}}})[{}][{}] // -> {} // structure: // { // '[object Object]': { // '[object Object]': {} // } // }
Подробнее об объектных читайте здесь:
- Инициализатор объекта на MDN
- JavaScript спецификация 12.2.6 Инициализатор объектов
Доступ к прототипам с помощью __proto__
Порой JavaScript элементы поражают. Как мы знаем, примитивы не имеют прототипов. Но если мы попытаемся получить значение примитивов __proto__, мы получим следующее:
(1).__proto__.__proto__.__proto__ // -> null
Объяснение:
Это происходит потому, что, когда что-то не имеет прототипа, оно будет завернуто в оболочку с использованием метода ToObject. Итак, шаг за шагом:
(1).__proto__ // -> [Number: 0] (1).__proto__.__proto__ // -> {} (1).__proto__.__proto__.__proto__ // -> null
Вот более подробная информация о __proto__:
- B.2.2.1 Object.prototype.proto
- JavaScript спецификация 7.1.13 ToObject (argument)
`${{Object}}`
Каков результат приведенного ниже выражения?
`${{Object}}`
Ответ:
// -> '[object Object]'
Объяснение:
Мы определили объект со свойством Object, используя Shorthand property notation:
{ Object: Object }
Затем мы передали этот объект в шаблонный литерал, так что метод toString вызывает данный объект. Вот почему мы получаем строку '[object Object]'.
- JavaScript спецификация 12.2.9 Шаблонные литералы
- Инициализатор объекта на MDN
Деструктурирование со значениями по умолчанию
Рассмотрим этот пример:
let x, {x: y = 1} = {x}; у;
Отличная задача для собеседования. Какое значение y? Ответ:
// -> 1
Объяснение:
let x, {x: y = 1} = {x}; у; // ↑ ↑ ↑ ↑ // 1 3 2 4
В приведенном выше примере:
- Мы объявляем x без значения, поэтому он не определен.
- Затем мы упаковываем значение x в свойство объекта x.
- Затем мы извлекаем значение x, используя деструктурирование, и хотим назначить его y. Если значение не определено, мы будем использовать 1 в качестве значения по умолчанию.
- Возвращаем значение y.
- Инициализатор объекта на MDN
Такие JavaScript элементы, как точки и их расширение
Интересные примеры могут быть составлены с расширением массивов. Учтем это:
[...[...'...']].length // -> 3
Объяснение:
Почему 3? Когда мы используем оператор расширения, вызывается метод @@iterator, и возвращаемый итератор используется для получения значений, которые нужно повторить. По умолчанию итератор для строки расширяет строку символами. После расширения мы собираем эти символы в массив. Затем мы снова расширяем данный массив и снова упаковываем его в массив.
Строка '...' состоит из трех точек (символов), поэтому длина результирующего массива равна трем.
Теперь, шаг за шагом:
[...'...'] // -> [ '.', '.', '.' ] [...[...'...']] // -> [ '.', '.', '.' ] [...[...'...']].length // -> 3
Очевидно, что мы можем распределять элементы массива столько раз, сколько хотим:
[...'...'] // -> [ '.', '.', '.' ] [...[...'...']] // -> [ '.', '.', '.' ] [...[...[...'...']]] // -> [ '.', '.', '.' ] [...[...[...[...'...']]]] // -> [ '.', '.', '.' ] // и так далее …
Labels
Не так много программистов знают о метках в 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. Удивлены?
Объяснение:
Относится ли это к множественному наследованию?
Некоторые JavaScript элементы заставляют серьезно поразмыслить. Взгляните на представленный код:
new (class F extends (String, Array) { }) // -> F []
Является ли это множественным наследованием? Нет.
Объяснение:
Значение extends ((String, Array)) представляет собой любопытную часть кода. Оператор группировки всегда возвращает свой последний аргумент, поэтому (String, Array) – это на самом деле просто Array. Значит, мы создали класс, который просто наследует массив.
Генератор
Рассмотрим пример генератора:
(function* f() { yield f })().next() // -> { value: [GeneratorFunction: f], done: false }
Как вы можете видеть, возвращаемое значение – это объект со значением, равным 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 } // и так далее // ...
Объяснение:
Предлагаем к прочтению такие разделы спецификации:
Класс класса
И снова JavaScript элементы ведут себя вне законов логики. Рассмотрим запутанный синтаксис:
(typeof (new (class { class () {} }))) // -> 'object'
Кажется, мы объявляем класс внутри класса. Должны быть и ошибки, однако, мы получаем строку 'object'.
Объяснение:
Начиная с эпохи ECMAScript 5, ключевые слова допускаются как имена свойств. Поэтому подумайте об этом простом примере:
const foo = { class: function() {} };
И ES6 стандартизовал сокращенные определения методов. Кроме того, классы могут быть анонимными. Поэтому, если мы отбросим : function, то получим:
class { class() {} }
Результат класса по умолчанию всегда является простым объектом. И его typeof должно возвращать 'object'.
Подробнее здесь:
Non-coercible objects
С известными символами есть способ избавиться от принуждения типа JS. Взгляните:
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 }
Теперь мы можем использовать это следующим образом:
// JavaScript элементы // объекты 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 вернется undefined.
Трудное возвращение
С return тоже не все так чисто. Учтите данный нюанс:
(function () { return { b : 10 } })() // -> undefined
Объяснение:
Дело в том, что return и возвращаемое выражение должны находиться в одной строке:
(function () { return { b : 10 } })() // -> { b: 10 }
Доступ к свойствам объекта с использованием массивов
Такие JavaScript элементы как массивы заслуживают отдельного раздела «необъяснимо, но факт». Тем не менее, затронем мы их здесь:
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 сообществу.