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



    Всем привет! Недавно услышал, как одни молодые фронтендеры пытались объяснить другим молодым фронтендерам, что такое 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 (как видно из примеров выше).
    Туту.ру 100,54
    Tutu.ru — сервис путешествий №1 в России.
    Поделиться публикацией
    Комментарии 18
    • +7
      Определение нужно было оставить на конец, а то после него уже не интересно читать.

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


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

      Нужно посоветовать спрашивать определение термина реверс-инженеринг на собеседовании.
      • 0
           var foo = Object.freeze({bar: 1});
           delete foo.bar;

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

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

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

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

          А так будет еще слегка короче, да и запись клевее смотрится))
          Function.prototype.apply
          // =>
          Reflect.apply
          // =>
          (_=>_).apply
          
          • +2
            (_=>_).apply


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


            Согласен что выглядит красиво. Но… ;)
            • +2
              вот и настал век программирования на смайликах...
            • +1
              Прям как рисование поп в Scala,
              lineLengths.reduce(_+_)
              
              • +1
                Кстати, если серьезно, то Reflect.apply появился ведь не «потому что это логично, чтобы функция была в Reflect», а потому-что apply есть у всех конструкторов. То есть путь даже может быть такой:

                Function.prototype.apply
                // =>
                Function.apply
                // =>
                Reflect.apply
                // =>
                Number.apply
                // =>
                Array.apply
                // =>
                Date.apply
                // =>
                Map.apply
                


                Из этих всех найболее сбалансированной по логичности/краткости мне кажется именно "Function.apply"
              • +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)
                • 0
                  Статья не про стиль. Но да, я согласен с вами, что ваш код читаемее. В продакшене есть линтеры и стайлгайды. Когда пишу для себя — мне просто так проще, я так мыслю.
                • +1
                  Это позволяет обрабатывать ошибки через условия, а не try-catch.

                  Это следует трактовать как дополнительную возможность или обработка ошибок через условие имеет какие-то явные преимущества? Просто интересуюсь. :)
                  • 0
                    Исторически мы привыкли, что блоки try catch проседают по перфомансу. Поэтому многие их избегают.
                    Ну и отлов ошибок через try catch более громоздкий
                    • +1
                      Исторически мы привыкли, что блоки try catch проседают по перфомансу. Поэтому многие их избегают.

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


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


                      Benchmarks

                      image


                      Ну и отлов ошибок через try catch более громоздкий

                      Зато он понятнее в разы, предположим, что я определяю в дескрипторе сразу несколько свойств, и происходит ошибка (TypeError: Cannot define property xxx, object is not extensible), в случае ошибки, можно понять, что именно произошло, а вот false мне мало о чем скажет.

                  • +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

                      Я далек от современного жс. Скажите, пожалуйста, почему в этом примере нельзя без Reflect?


                      const $ = selector =>
                        new Proxy(
                          document.querySelector(selector)||Element,
                          { get: (target, key) => target[key] }
                         )
                      ;
                      
                      // Или даже без Proxy:
                      const $ = selector => document.querySelector(selector)||Element
                      • 0
                        С прокси больше возможностей по перехвату. Сделать без прокси и рефлекта — пожалуйста. И да, это простые примеры. Так же как для примеров в ФП показывают частное применние на примере функций add()() где не виден смысл. Более сложные примеры сложнее объяснять. Никто не мешает написать 10 строк кода с if'ами и try-catch'ами. Все так же будет работать. Наверное с jQ не самый удачный пример оказался.
                      • +1
                        Object.getPrototypeOf(1); // undefined

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

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое