javascript

Про Reflect API доступным языком

  • среда, 2 августа 2017 г. в 03:13:14
https://habrahabr.ru/company/tuturu/blog/334546/
  • JavaScript
  • Блог компании Туту.ру




Всем привет! Недавно услышал, как одни молодые фронтендеры пытались объяснить другим молодым фронтендерам, что такое Reflect в JavaScript. В итоге кто-то сказал, что это такая же штука, как прокси. Ситуация напомнила мне анекдот:

Встречаются два майнера:
— Ты что-нибудь понимаешь в этом?
— Ну объяснить смогу.
— Это понятно, но ты что-нибудь понимаешь в этом?

Вот и с Reflect в JS для кого-то получилась такая же ситуация. Вроде бы что-то говорят, а для чего —  непонятно. В итоге я подумал, что стоит об этом рассказать еще раз простым языком с примерами.

Сначала дадим определение, что такое рефлексия в программировании:
Reflection/Reflect API  —  это API, который предоставляет возможность проводить реверс-инжиниринг классов, интерфейсов, функций, методов и модулей.

Отсюда становится немного понятнее, для чего это API должно использоваться. Reflection API существует в разных языках программирования и, порой, используется для обхода ограничений, накладываемых ЯП. Также он используется для разработки различных вспомогательных утилит и для реализации различных паттернов (таких как Injection) и много чего еще.

Например, Reflection API есть в Java. Он используется для просмотра информации о классах, интерфейсах, методах, полях, конструкторах и аннотациях во время выполнения java программ. К примеру, с помощью Reflection в Java можно использовать ООП паттерн  —  Public Morozov.

В PHP тоже существует Reflection API, который позволяет не только делать реверс-инжиниринг, но даже позволяет получать doc-блоки комментариев, что используется в различных системах автодокументирования.

В JavaScript Reflect  —  это встроенный объект, который предоставляет методы для перехватывания JavaScript операций. По сути, это неймспейс (как и Math). Reflect содержит в себе набор функций, которые называются точно так же, как и методы для Proxy.

Некоторые из этих методов  —  те же, что и соответствующие им методы класса Object или Function. JavaScript растет и превращается в большой и сложный ЯП. В язык приходят различные вещи из других языков. На сегодня Reflect API умеет не так много, как в других ЯП. Тем не менее, есть предложения по расширению, которые еще не вошли в стандарт, но уже используются. Например, Reflection Metadata.

Можно сказать, что неймспейс Reflect в JS  —  это результат рефакторинга кода. Мы уже пользовались ранее возможностями Reflect API, просто все эти возможности были вшиты в базовый класс   Object.

Reflect Metadata / Metadata Reflection


Это API создано для получения информации об объектах в рантайме. Это proposal, который пока не является стандартом. Сейчас активно используется полифил. На сегодняшний день активно применяется в Angular. С помощью этого API реализованы Inject и декораторы (анотаторы).

Собственно ради Angular в TypeScript был добавлен расширенный синтаксис декораторов. Одной из интересных особенностей декораторов является возможность получать информацию о типе декорируемого свойства или параметра. Чтобы это заработало, нужно подключить библиотеку reflect-metadata, которая расширяет стандартный объект Reflect и включить опцию emitDecoratorMetadata к конфиге TS. После этого для свойств, которые имеют хотя бы один декоратор, можно вызвать Reflect.getMetadata с ключом «design:type».

В чем различие Reflect от Proxy?


Reflect  —  это набор полезных методов для работы с объектами, половина которых  —  это переписанные уже существующие из Object. Сделано это с целью улучшения семантики и наведения порядка, так как Object  —  это базовый класс, но при этом он содержит очень много методов, которые не должны в нем находиться. Также если вы создаете объект с пустым прототипом, то у вас исчезают методы рефлексии (ниже покажу на примере, что это значит).

Proxy — это класс, который всегда создает новый объект с установленными обработчиками для перехвата доступа. Он позволяет отлавливать любые действия с объектом и модифицировать их. Для реализации различной логики часто применяется Reflect. Ниже на примерах это будет хорошо видно.

Use Cases


Ну и рассмотрим способы применения Reflect API. Некоторые примеры уже давно известны, просто для этих целей мы привыкли использовать методы из класса Object. Но было бы правильнее, по логике, использовать их из пакета Reflect (пакеты  —  терминология из Java).

Автогенерируемые поля объекта


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

const emptyObj = () =>
 new Proxy({},
   {
     get: (target, key, receiver) => (
           Reflect.has(target, key) ||
           Reflect.set(target, key, emptyObj()),
           Reflect.get(target, key, receiver)
     )
   }
 )
;
const path = emptyObj();

path.to.virtual.node.in.empty.object = 123;

console.log(path.to.virtual.node.in.empty.object); // 123

Все круто, но такой объект нельзя сериализовать в JSON, получим ошибку. Добавим магический метод сериализации  —  toJSON

console.clear();
const emptyObj = () =>
 new Proxy({},
   {
     get: (target, key, receiver) => (
        key == 'toJSON'
          ? () => target
          : (
              Reflect.has(target, key) ||
              Reflect.set(target, key, emptyObj()),
              Reflect.get(target, key, receiver)
            )
     )
   }
 )
;
const path = emptyObj();
path.to.virtual.node.in.empty.object = 123;

console.log(JSON.stringify(path));
// {"to":{"virtual":{"node":{"in":{"empty":{"object":123}}}}}}

Динамический вызов конструктора


Имеем:

var obj = new F(...args)

Но хотим уметь динамически вызывать конструктор и создавать объект. Для этого есть Reflect.construct:

var obj = Reflect.construct(F, args)

Может понадобиться для использования в фабриках (ООП гайз поймут). Пример:

// Old method
function Greeting(name) { this.name = name }
Greeting.prototype.greet = function() { return `Hello ${this.name}` }

function greetingFactory(name) {
   var instance = Object.create(Greeting.prototype);
   Greeting.call(instance, name);
   return instance;
}

var obj = greetingFactory('Tuturu');
obj.greet();

Как такое пишется в 2017 году:

class Greeting {
   constructor(name) { this.name = name }
   greet() { return `Hello ${this.name}` }
}

const greetingFactory = name => Reflect.construct(Greeting, [name]);

const obj = greetingFactory('Tuturu');
obj.greet();

Повторяем поведение jQuery


Следующая строка показывает как можно сделать jQuery в 2 строки:

const $ = document.querySelector.bind(document);
Element.prototype.on = Element.prototype.addEventListener;

Удобно, если нужно что-то быстро наваять без зависимостей, а писать длинные нативные конструкции лень. Но в этой реализации есть минус — выбрасывает исключение при работе с null:

console.log( $('some').innerHTML );
error TypeError: Cannot read property 'innerHTML' of null

Используя Proxy и Reflect можем переписать этот пример:

const $ = selector =>
  new Proxy(
    document.querySelector(selector)||Element,
    { get: (target, key) => Reflect.get(target, key) }
   )
;

Теперь при попытке обращения к null свойствам просто будем получать undefined:

console.log( $('some').innerHTML ); // undefined

Так почему же надо использовать Reflect?


Reflect API более удобен при обработке ошибок. К примеру, всем знакома инструкция:
Object.defineProperty(obj, name, desc)

В случае неудачи будет выброшено исключение. А вот Reflect не генерит исключений на все подряд, а умеет возвращать булев результат:

try {
   Object.defineProperty(obj, name, desc);
   // property defined successfully
} catch (e) {
   // possible failure (and might accidentally catch the wrong exception)
}
/* --- OR --- */
if (Reflect.defineProperty(obj, name, desc)) {
   // success
} else {
   // failure
}

Это позволяет обрабатывать ошибки через условия, а не try-catch. Пример применения Reflect API с обработкой ошибки:

try {
   var foo = Object.freeze({bar: 1});
   delete foo.bar;
} catch (e) {}

А теперь можно писать так:

var foo = Object.freeze({bar: 1});
if (Reflect.deleteProperty(foo, 'bar')) {
   console.log('ok');
} else {
   console.log('error');
}

Но надо сказать, что есть случаи, когда Reflect также выбрасывает исключения.

Некоторые записи выходят короче


Без лишних слов:

Function.prototype.apply.call(func, obj, args)
/* --- OR --- */
Reflect.apply.call(func, obj, args)

Разница в поведении


Пример без слов:

Object.getPrototypeOf(1); // undefined
Reflect.getPrototypeOf(1); // TypeError

Вроде бы все понятно. Делаем выводы, что лучше. Reflect API более логичный.

Работа с объектами с пустым прототипом


Дано:

const myObject = Object.create(null);
myObject.foo = 123;

myObject.hasOwnProperty === undefined; // true

// Поэтому приходится писать так:
Object.prototype.hasOwnProperty.call( myObject, 'foo' ); // true

Как видите, мы уже не имеем методов рефлексии, например, hasOwnProperty. Поэтомы мы либо пользуемся старым способом, обращаясь к прототипу базового класса, либо обращаемся к Reflect API:

Reflect.ownKeys(myObject).includes('foo') // true

Выводы


Reflect API — это результат рефакторинга. В этом неймспейсе содержатся функции рефлексии, которые раньше были зашиты в базовые классы Object, Function… Изменено поведение и обработка ошибок. В будущем этот неймспейс будет расширяться другими рефлективными инструментами. Так же Reflect API можно считать неотъемлемой частью при работе с Proxy (как видно из примеров выше).