Всем привет! Недавно услышал, как одни молодые фронтендеры пытались объяснить другим молодым фронтендерам, что такое 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 (как видно из примеров выше).
Комментарии (18)
Apathetic
01.08.2017 13:34var foo = Object.freeze({bar: 1}); delete foo.bar;
Давно delete начал ошибку выбрасывать?
TheShock
01.08.2017 14:05+3Некоторые записи выходят короче
А так будет еще слегка короче, да и запись клевее смотрится))
Function.prototype.apply // => Reflect.apply // => (_=>_).apply
TheShock
02.08.2017 15:24+1Кстати, если серьезно, то
Reflect.apply
появился ведь не «потому что это логично, чтобы функция была в Reflect», а потому-что apply есть у всех конструкторов. То есть путь даже может быть такой:
Function.prototype.apply // => Function.apply // => Reflect.apply // => Number.apply // => Array.apply // => Date.apply // => Map.apply
Из этих всех найболее сбалансированной по логичности/краткости мне кажется именно "Function.apply
"
SuperPaintman
01.08.2017 18:48+6Я понимаю, что сказанное ниже это вкусовщина (
просто устал это видель в open-source проектах), но вы действительно считаете, что это:
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 emptyObj = () => ( new Proxy({}, { get(target, key, receiver) { if (key === 'toJSON') { return () => target; } if (!Reflect.has(target, key)) { Reflect.set(target, key, emptyObj()); } return Reflect.get(target, key, receiver); } }) );
У вас 15 строчек, и у меня 15 строчек, но ИМХО, в if'ах нет ничено плохого, используйте логические операторы по их назначению.
Визуально отделить эту запятую весьма непросто:
Reflect.has(target, key) || Reflect.set(target, key, emptyObj()), // <== Reflect.get(target, key, receiver)
0xy
01.08.2017 20:03Статья не про стиль. Но да, я согласен с вами, что ваш код читаемее. В продакшене есть линтеры и стайлгайды. Когда пишу для себя — мне просто так проще, я так мыслю.
tasty_brains
02.08.2017 10:11+1Это позволяет обрабатывать ошибки через условия, а не try-catch.
Это следует трактовать как дополнительную возможность или обработка ошибок через условие имеет какие-то явные преимущества? Просто интересуюсь. :)0xy
02.08.2017 10:12Исторически мы привыкли, что блоки try catch проседают по перфомансу. Поэтому многие их избегают.
Ну и отлов ошибок через try catch более громоздкийSuperPaintman
02.08.2017 11:44+1Исторически мы привыкли, что блоки try catch проседают по перфомансу. Поэтому многие их избегают.
Это утверждение верно только для v8 ниже 5.8 (январь 2017, если не изменяет память) / Node.js v8.x.x. Да, конечно, не у всех еще обновлен хром / node до этих версий, тем не менее проблема решена, и ее доля в будущем будет только уменьшаться.
Ну и да, так как Reflect API появился достаточно недавно, придется использовать поллифил, а там try / catch.
Benchmarks
torbasow
02.08.2017 12:27+1Я тут не ухватываю чего-то главного, из-за чего назначение Reflect остаётся загадкой.
Почему вместо
( Reflect.has(target, key) || Reflect.set(target, key, emptyObj()), Reflect.get(target, key, receiver) )
нельзя или плохо просто вот так:
{ if(!(key in target)){ target[key] = emptyObj(); } return target[key]; }
printercu
02.08.2017 12:43+1Я далек от современного жс. Скажите, пожалуйста, почему в этом примере нельзя без Reflect?
const $ = selector => new Proxy( document.querySelector(selector)||Element, { get: (target, key) => target[key] } ) ; // Или даже без Proxy: const $ = selector => document.querySelector(selector)||Element
0xy
02.08.2017 13:02С прокси больше возможностей по перехвату. Сделать без прокси и рефлекта — пожалуйста. И да, это простые примеры. Так же как для примеров в ФП показывают частное применние на примере функций add()() где не виден смысл. Более сложные примеры сложнее объяснять. Никто не мешает написать 10 строк кода с if'ами и try-catch'ами. Все так же будет работать. Наверное с jQ не самый удачный пример оказался.
Bhudh
03.08.2017 10:53+1Object.getPrototypeOf(1); // undefined
Не поверил. Проверил.
Хром 49: Number
Лис 52.2: Number
Опера 12.18: Unhandled Error: Object.getPrototypeOf: first argument not an Object
В каком undefined выдаёт?
vasIvas
Определение нужно было оставить на конец, а то после него уже не интересно читать.
Ну конечно, это реверс-инженеринг, пора расходится.
Нужно посоветовать спрашивать определение термина реверс-инженеринг на собеседовании.