Reflect API Pro in an accessible language



    Hello! Recently I heard how some young front-endenders tried to explain to other young front-endors what Reflect in JavaScript is. As a result, someone said that this is the same thing as a proxy. The situation reminded me of a joke:

    Two miners meet:
    - Do you understand anything about this?
    - Well, I can explain.
    “It's understandable, but do you understand anything about this?”

    So with Reflect in JS, for someone, the same situation turned out. It seems to be saying something, but for what purpose it is not clear. In the end, I thought it was worth telling about it again in a simple language with examples.

    First, let's define what reflection is in programming:
    The Reflection / Reflect API is an API that provides the ability to reverse engineer classes, interfaces, functions, methods and modules.

    From here it becomes a little clearer why this API should be used. The Reflection API exists in various programming languages ​​and, at times, is used to circumvent the limitations imposed by the PL. It is also used to develop various helper utilities and to implement various patterns (such as Injection) and much more.

    For example, the Reflection API is in Java. It is used to view information about classes, interfaces, methods, fields, constructors and annotations during the execution of java programs. For example, using Reflection in Java, you can use the OOP pattern -  Public Morozov .

    In PHP, there is also a Reflection API, which allows you to not only do reverse engineering, but even allows you to receive doc blocks of comments, which is used in various self-documenting systems.

    In JavaScript, Reflect is a built-in object that provides methods for intercepting JavaScript operations. In fact, this is a namespace (like Math). Reflect contains a set of functions that are called exactly like the methods for Proxy.

    Some of these methods are the same as the corresponding methods of the Object or Function class. JavaScript grows and turns into a large and complex JP. Various things from other languages ​​come into the language. Today, the Reflect API is not as capable as in other languages. However, there are expansion proposals that are not yet part of the standard, but are already in use. For example, Reflection Metadata.

    We can say that the Reflect namespace in JS is the result of code refactoring. We already used the Reflect API features before, just all these features were embedded in the base Object class.

    Reflect Metadata / Metadata Reflection


    This API is designed to retrieve information about objects in runtime. This is a proposal, which is not yet the standard. Polyphil is now actively used. Today it is actively used in Angular. Using this API, Inject and decorators (anotators) are implemented.

    Actually, for the sake of Angular, TypeScript has been expanded with extended decorator syntax. One of the interesting features of decorators is the ability to obtain information about the type of decorated property or parameter. For this to work, you need to connect the reflect-metadata library, which extends the standard Reflect object and enable the emitDecoratorMetadata option in the TS config. After that, for properties that have at least one decorator, you can call Reflect.getMetadata with the key "design: type".

    What is the difference between Reflect and Proxy?


    Reflect  is a set of useful methods for working with objects, half of which are rewritten existing from Object. This was done in order to improve semantics and restore order, since Object is a base class, but at the same time it contains a lot of methods that should not be in it. Also, if you create an object with an empty prototype, then your reflection methods disappear (below I will show with an example what this means).

    Proxy is a class that always creates a new object with handlers installed to intercept access. It allows you to catch any actions with the object and modify them. Reflect is often used to implement various logic. Below with examples, this will be clearly visible.

    Use cases


    Well, let's look at ways to use the Reflect API. Some examples have long been known, just for these purposes we are used to using methods from the Object class. But it would be more correct, logically, to use them from the Reflect package (packages - terminology from Java).

    Auto generated object fields


    We can create an object in which the fields of the object will be created automatically during access to them.

    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
    

    Everything is cool, but such an object cannot be serialized in JSON, we get an error. Add a magic serialization method - 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}}}}}}
    

    Dynamic constructor call


    We have:

    var obj = new F(...args)
    

    But we want to be able to dynamically call the constructor and create an object. There is Reflect.construct for this:

    var obj = Reflect.construct(F, args)
    

    It may be needed for use in factories (OOP gays will understand). Example:

    // 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();
    

    How is it written in 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();
    

    Repeat jQuery Behavior


    The following line shows how to make jQuery in 2 lines:

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

    It is convenient if you need to quickly build something without dependencies, and lazy to write long native constructions. But in this implementation there is a minus - it throws an exception when working with null:

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

    Using Proxy and Reflect we can rewrite this example:

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

    Now, when we try to access null properties, we just get undefined:

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

    So why use Reflect?


    Reflect API is more convenient for error handling. For example, the instruction is familiar to everyone:
    Object.defineProperty (obj, name, desc)

    In case of failure, an exception will be thrown. But Reflect does not throw exceptions for everything, but can return a Boolean result:

    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
    }
    

    This allows errors to be handled through conditions, rather than try-catch. An example of using the Reflect API with error handling:

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

    And now you can write like this:

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

    But I must say that there are cases when Reflect also throws exceptions.

    Some entries are shorter


    Without many words:

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

    Difference in behavior


    An example without words:

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

    Everything seems to be clear. We conclude that it is better. Reflect API is more logical.

    Work with objects with an empty prototype


    Given:

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

    As you can see, we no longer have reflection methods, for example, hasOwnProperty. Therefore, we either use the old way, referring to the prototype of the base class, or refer to the Reflect API:

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

    conclusions


    The Reflect API is the result of refactoring. This namespace contains reflection functions that were previously wired to the base classes Object, Function ... Behavior and error handling changed. In the future, this namespace will expand with other reflective tools. Also, the Reflect API can be considered an integral part when working with Proxy (as can be seen from the examples above).

    Also popular now: