ES6: отражение изнутри
Перевод статьи ES6 Reflection in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.
О, привет, я просто готовился и не заметил вас! Встречайте очередную часть ES6 — «Уфф, наконец-то мы прошли ловушки» — изнутри. Не слышали об этом? Тогда загляните в краткую историю инструментария ES6. Затем изучите деструктирование, литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры, улучшения в литералах объекта, новый «сахарок» — классы поверх прототипов, let, const и «Временную мёртвую зону», а также итераторы, генераторы, символы, объекты Map, WeakMaps, Sets и WeakSets, прокси, ловушки прокси, и ещё о ловушках. Ну а сегодня поговорим об API встроенного объекта Reflect
.
Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощью интерактивной оболочки REPL, либо командной строки babel-node и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6. Ещё одна довольно полезная альтернатива, это использовать онлайновый REPL для Babel — он показывает скомпилированный ES5-код справа от ES6-кода, чтобы быстро сравнить.
Пока мы не начали, позвольте беззастенчиво попросить вашей поддержки, если вы наслаждаетесь моей серией «ES6 изнутри». Ваши взносы пойдут на хостинг сайту, мне на еду, на то, чтоб я не отставал от графика, и на то, чтобы Pony Foo оставался поистине источником джаваскриптовых вкусняшек.
Спасибо, что выслушали, а теперь перейдём к Reflect! Советую прочитать статьи про прокси: встроенный объект Proxy, ловушки и ещё ловушки. Они помогут понять некоторые вещи, о которых мы сегодня поговорим.
Зачем отражение?
Многие статически типизированные языки уже давно предложили API отражения (например, в Python или в в C#), тогда как JavaScript вряд ли нуждается в этом — он уже динамический язык. С приходом ES6 появилось еще несколько мест, где разработчикам стали доступны аспекты языка, ранее бывшие чисто внутренними — да, я говорю про Proxy
.
Можно сказать, что в JavaScript уже есть функции отражения в ES5, даже при том, что ни спецификация, ни сообщество их так не называли. Методы Array.isArray
, Object.getOwnPropertyDescriptor
и даже Object.keys
можно считать классическими примерами, относящимися к категории отражения в других языках. В дальнейшем, новые методы из этой категории «поселятся» во встроенном объекте Reflect
. Что вполне разумно, не так ли? Зачем вам в Object
статические методы вроде getOwnPropertyDescriptor
(или даже create
), которые как две капли воды похожи на отражение? В конце концов Object
задуман как базовый прототип, а не как хранилище методов отражения. Выделенный интерфейс, который предоставляет большинство методов отражения, куда более полезный.
Объект Reflect
В последние несколько дней мы уже затрагивали объект Reflect
вскользь. Подобно Math
, Reflect
— это статический объект, его нельзя создать в конструкторе оператором new
или вызвать методом call
, и все его методы статические. _ловушки в прокси ES6 (рассмотренные здесь и здесь) соотносятся один к одному с API отражения. Для каждой ловушки есть соответствующий метод отражения в Reflect
.
У API отражения в JavaScript есть ряд преимуществ, которые стоит рассмотреть.
Возвращаемые значения в Reflect
и в отражении средствами Object
У методов Reflect
более осмысленные возвращемые значения, чем у соответствующих им методов отражения из Object
. Например, метод Reflect.defineProperty
возвращает булевое значение, указывающее, успешно ли определено свойство или нет. При этом его эквивалент Object.defineProperty
возвращает объект, полученный в качестве первого аргумента — что не совсем полезно.
Пример ниже показывает, как убедиться, работает ли Object.defineProperty или нет.
try { Object.defineProperty(target, 'foo', { value: 'bar' }) // ура! } catch (e) { // упс. }
Тогда как с Reflect.defineProperty
всё получается гораздо естественнее
var yay = Reflect.defineProperty(target, 'foo', { value: 'bar' }) if (yay) { // yay! } else { // упс. }
Тем самым мы избежали блока try/catch
и код стало легче поддерживать в процессе.
Полноценная замена ключевым словам
Некоторые из этих методов отражения предоставляют программные альтернативы для задач, которые ранее решались только с помощью ключевых слов. Например, Reflect.deleteProperty(target, key)
— эквивалент выражения delete target[key]
. До ES6 для того, чтобы вызов метода привёл к вызову delete
, приходилось создавать специальный вспомогательный метод, оборачивающий delete
от вашего имени.
var target = { foo: 'bar', baz: 'щито' } delete target.foo console.log(target) // <- { baz: 'щито' }
Тогда как сегодня, с ES6, такой метод уже есть в Reflect.deleteProperty
.
var target = { foo: 'bar', baz: 'щито' } Reflect.deleteProperty(target, 'foo') console.log(target) // <- { baz: 'щито' }
Как и в случае с deleteProperty
, есть несколько других методов, которые упрощают и другие вещи.
Легче сочетать new с произвольными списками аргументов
В ES5 это трудная задача: как создать new Foo
, передавая произвольное число аргументов? Нельзя сделать это напрямую, а решение обходным путем получается очень длинным. Придётся создать промежуточный объект, который получит переданные аргументы в качестве Array
. Потом вы заставляете конструктор этого объекта вернуть результат, который изначально предназначался для .apply
. Элементарно же, правда? Что значит нет?
var proto = Dominus.prototype Applied.prototype = proto function Applied (args) { return Dominus.apply(this, args) } function apply (a) { return new Applied(a) }
К счастью, использовать apply
довольно просто.
apply(['.foo', '.bar']) apply.call(null, '.foo', '.bar')
Но это безумие, правда? Кто так делает? Ну, в ES5 — каждый, у кого на это есть веская причина! К счастью, у ES6 есть менее безумные подходы к этой задаче. Один из них — просто использовать оператор расширения.
new Dominus(...args)
Другая альтернатива — Reflect
.
Reflect.construct(Dominus, args)
Оба эти подхода значительно проще, чем то, что мне пришлось делать в коде dominus
.
Применение функций с помощью apply
, правильный способ
В ES5, чтобы вызвать метод с произвольным числом аргументов, можно использовать .apply
, передавая контекст this
и ваши аргументы.
fn.apply(ctx, [1, 2, 3])
Если мы боимся, что fn
перекроет apply
своим собственным свойством, можно рассчитывать на более безопасную, но более многословную альтернативу.
Function.prototype.apply.call(fn, ctx, [1, 2, 3])
В ES6 можно использовать оператор расширения в качестве альтернативы .apply
для произвольного числа аргументов.
fn(...[1, 2, 3])
Хотя это не выход, если нужно определить контекст this
. Можно вернуться к способу с Function.prototype
, но он слишком многословен. Вот как может помочь Reflect
.
Reflect.apply(fn, ctx, args)
Конечно, одно из самых подходящих применений для методов API Reflect
— поведение по умолчанию в ловушках Proxy
.
Поведение по умолчанию в ловушках Proxy
Мы уже обсуждали, как ловушки соотносятся один к одному с методами Reflect
. Но не коснулись того, что их интерфейсы также соотносятся. Иными словами, их аргументы и возвращаемые значения совпадают. Это означает, что в коде можно делать что-то вроде этого, чтобы получить поведение по умолчанию ловушки get
в обработчиках прокси.
var handler = { get () { return Reflect.get(...arguments) } } var target = { a: 'b' } var proxy = new Proxy(target, handler) console.log(proxy.a) // <- 'b'
Нет, правда, ничто не помешает вам сделать этот обработчик ещё проще. Конечно, на этот этапе было бы лучше убрать ловушку полностью.
var handler = { get: Reflect.get }
Здесь важно то, что можно настроить ловушку в обработчике прокси, подключив некоторый пользовательский функционал, который в итоге выбросит ошибку или запишет сообщение в консоль, а затем в случае по умолчанию можно просто использовать следующий однострочный способ.
return Reflect[trapName](...arguments)
Теперь мне будет намного спокойнее при раскрытии тайн Proxy
.
И наконец, есть __proto__
Вчера мы говорили, что устаревший __proto__
— часть спецификации ES6, но всё-таки я отговаривал от него, и что вместо него нужно использовать Object.setPrototypeOf
и Object.getPrototypeOf
. Оказывается, для этих методов есть также аналоги Reflect
. Подумайте об этих методах, как о геттерах и сеттерах для __proto__
, но без кроссбраузерных расхождений.
Я бы прямо сейчас не стал гнаться за модой применять
setPrototypeOf
для всего подряд. Вообще-то я надеюсь, что такой моды и не будет, должен признаться.
P.S. Это тоже может быть интересно: