CSS-live.ru

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. Это тоже может быть интересно:

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.