Всем привет! Недавно услышал, как одни молодые фронтендеры пытались объяснить другим молодым фронтендерам, что такое 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)


  1. vasIvas
    01.08.2017 11:05
    +7

    Определение нужно было оставить на конец, а то после него уже не интересно читать.

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


    Ну конечно, это реверс-инженеринг, пора расходится.

    Нужно посоветовать спрашивать определение термина реверс-инженеринг на собеседовании.


  1. Apathetic
    01.08.2017 13:34

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

    Давно delete начал ошибку выбрасывать?


    1. vasIvas
      01.08.2017 13:41
      +3

      function test(){
          'use strict'; // куда без него?
      
          let foo = Object.freeze({bar: 1});
      
          delete foo.bar;
      }
      
      test(); // Uncaught TypeError: Cannot delete property 'bar' of #<Object>
      


      1. Apathetic
        01.08.2017 16:43

        Спасибо за напоминание =)


  1. TheShock
    01.08.2017 14:05
    +3

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

    А так будет еще слегка короче, да и запись клевее смотрится))

    Function.prototype.apply
    // =>
    Reflect.apply
    // =>
    (_=>_).apply
    


    1. 0xy
      01.08.2017 14:18
      +2

      (_=>_).apply


      Я бы тогда предложил:
      ((o)=>(o)).apply
      


      Согласен что выглядит красиво. Но… ;)


      1. vitvad
        02.08.2017 01:55
        +2

        вот и настал век программирования на смайликах...


    1. nbytes
      02.08.2017 08:25
      +1

      Прям как рисование поп в Scala,

      lineLengths.reduce(_+_)
      


    1. 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"


  1. 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)


    1. 0xy
      01.08.2017 20:03

      Статья не про стиль. Но да, я согласен с вами, что ваш код читаемее. В продакшене есть линтеры и стайлгайды. Когда пишу для себя — мне просто так проще, я так мыслю.


  1. tasty_brains
    02.08.2017 10:11
    +1

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

    Это следует трактовать как дополнительную возможность или обработка ошибок через условие имеет какие-то явные преимущества? Просто интересуюсь. :)


    1. 0xy
      02.08.2017 10:12

      Исторически мы привыкли, что блоки try catch проседают по перфомансу. Поэтому многие их избегают.
      Ну и отлов ошибок через try catch более громоздкий


      1. SuperPaintman
        02.08.2017 11:44
        +1

        Исторически мы привыкли, что блоки try catch проседают по перфомансу. Поэтому многие их избегают.

        Это утверждение верно только для v8 ниже 5.8 (январь 2017, если не изменяет память) / Node.js v8.x.x. Да, конечно, не у всех еще обновлен хром / node до этих версий, тем не менее проблема решена, и ее доля в будущем будет только уменьшаться.


        Ну и да, так как Reflect API появился достаточно недавно, придется использовать поллифил, а там try / catch.


        Benchmarks

        image


  1. 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];
    }
    


  1. 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


    1. 0xy
      02.08.2017 13:02

      С прокси больше возможностей по перехвату. Сделать без прокси и рефлекта — пожалуйста. И да, это простые примеры. Так же как для примеров в ФП показывают частное применние на примере функций add()() где не виден смысл. Более сложные примеры сложнее объяснять. Никто не мешает написать 10 строк кода с if'ами и try-catch'ами. Все так же будет работать. Наверное с jQ не самый удачный пример оказался.


  1. Bhudh
    03.08.2017 10:53
    +1

    Object.getPrototypeOf(1); // undefined

    Не поверил. Проверил.
    Хром 49: Number
    Лис 52.2: Number
    Опера 12.18: Unhandled Error: Object.getPrototypeOf: first argument not an Object
    В каком undefined выдаёт?