JavaScript Metaprogramming

  • Tutorial
Metaprogramming is a type of programming associated with the creation of programs that generate other programs as a result of their work, or programs that change themselves during execution. (Wikipedia)

In simpler language, metaprogramming in the framework of JavaScript can be considered mechanisms that allow analyzing and changing the program in real time, depending on any actions. And, most likely, you somehow use them when writing scripts every day.


JavaScript is by its nature a very powerful dynamic language and allows you to pleasantly write flexible code:


/**
 * Динамическое создание save-метода для каждого свойства
 */const comment = { authorId: 1, comment: 'Комментарий' };
for (let name in comment) {
    const pascalCasedName = name.slice(0, 1).toUpperCase() + name.slice(1);
    comment[`save${pascalCasedName}`] = function() {
        // Сохраняем поле
    }
}
comment.saveAuthorId(); // Сохраняем authorId
comment.saveComment(); // Сохраняем comment

Similar code for dynamically creating methods in other languages ​​very often may require a special syntax or API for this. For example, PHP is also a dynamic language, but in it it will require more effort:


<?phpclassComment{
    public $authorId;
    public $comment;
    publicfunction__construct($authorId, $comment){
        $this->authorId = $authorId;
        $this->comment = $comment;
    }
    // Перехватываем все вызовы методов в классеpublicfunction__call($methodName, $arguments){
        foreach (get_object_vars($this) as $fieldName => $fieldValue) {
            $saveMethodName = "save" . strtoupper($fieldName[0]) . substr($fieldName, 1);
            if ($methodName == $saveMethodName) {
                // Сохраняем поле
            }
        }
    }
}
$comment = new Comment(1, 'Комментарий');
$comment->saveAuthorId(); // Сохраняем authorId
$comment->saveComment(); // Сохраняем comment

In addition to a flexible syntax, we also have a bunch of useful functions for writing dynamic code: Object.create, Object.defineProperty, Function.apply, and many others.


Consider them in more detail.


  1. Code generation
  2. Work with functions
  3. Work with objects
  4. Reflect API
  5. Symbols (Symbols)
  6. Proxy (Proxy)
  7. Conclusion

1. Code generation


The standard tool for dynamic code execution is the eval function , which allows you to execute code from the transferred string:


eval('alert("Hello, world")');

Unfortunately, eval has many nuances:


  • if our code is written in 'use strict', then variables declared inside eval will not be visible in the calling eval code. At the same time, the code inside eval can always change external variables.
  • The code inside eval can be executed both in the global context (if called via window.eval), or in the context of the function within which the call occurred (if just eval, without window).
  • problems may arise due to the minification of JS, when variable names are replaced with shorter ones to reduce the size. The code transmitted as a string in eval usually doesn’t touch the mini-finisher, because of this we can start to access external variables using old non-modified names, which will lead to subtle errors.

To solve these problems there is a great alternative - new Function .


const hello = newFunction('name', 'alert("Hello, " + name)');
hello('Андрей') // alert("Hello, Андрей");

Unlike eval, we can always explicitly pass parameters through function arguments and dynamically point it to this context (via Function.apply or Function.call ). In addition, the function being created is always called in the global scope.


In the old days, eval was often used to dynamically change code, since JavaScript had very few mechanisms for reflection and it was impossible to do without eval. But in the modern standard of language, much more high-level functionality has appeared and eval is now used much less frequently.


2. Work with functions


JavaScript provides us with many excellent tools for dynamically working with functions, allowing us to both receive various information about a function in runtime and also change it:


  • Function.length - allows you to find out the number of arguments to the function:


    const func = function(name, surname) { 
        console.log(`Hello, ${surname}${name}`)
    };
    console.log(func.length) // 2

  • Function.apply and Function.call - allow you to dynamically change the context of this function:


    const person = {
        name: 'Иван',
        introduce: function() {
            return${this.name}`;
        }
    }
    person.introduce(); // Я Иван
    person.introduce.call({ name: 'Егор' }); // Я Егор

    They differ from each other only in the fact that in Function.apply the arguments of the function are given as an array, and in Function.call - separated by commas. This feature used to be often used to pass a list of arguments to a function as an array. A common example is the Math.max function (by default, it does not know how to work with arrays):


    Math.max.apply(null, [1, 2, 4, 3]); // 4

    With the advent of the new spread-operator, you can simply write like this:


    Math.max(...[1, 2, 4, 3]); // 4

  • Function.bind - allows you to create a copy of a function from an existing one, but with a different context:


    const person = {
        name: 'Иван',
        introduce: function() {
            return${this.name}`;
        }
    }
    person.introduce(); // Я Иванconst introduceEgor = person.introduce.bind({ name: 'Егор' });
    introduceEgor(); // Я Егор

  • Function.caller - allows you to get the calling function. It is not recommended to use it , as it is absent in the language standard and will not work in strict mode. This was done due to the fact that if different JavaScript engines implement the tail call optimization described in the language specification , the call to Function.caller may begin to produce incorrect results. Usage example:


    const a = function() {
        console.log(a.caller == b);
    }
    const b = function() {
        a();
    }
    b(); // true

  • Function.toString - returns a string representation of the function. This is a very powerful feature that allows you to explore both the contents of the function and its arguments:


    const getFullName = (name, surname, middlename) => {
        console.log(`${surname}${name}${middlename}`);
    }
    getFullName.toString()
    /*
     * "(name, surname, middlename) => {
     *     console.log(`${surname} ${name} ${middlename}`);
     * }"
     */

    After receiving the string representation of the function, we can parse it and analyze it. This can be used to, for example, pull out the function argument names and, depending on the name, automatically substitute the desired parameter. In general, you can parse in two ways:


    • Parsim bunch of regulars and get an acceptable level of reliability (may not work if we do not cover all possible types of function entries).
    • We get the string representation of the function and put it into the finished JavaScript parser (for example, esprima or acorn ), and then work with structured AST. An example of parsing in AST via esprima. I can also advise a good report about parsers from Alexey Okhrimenko.


Simple examples with parsing functions by regulars:


Getting a list of function arguments
/**
 * Получить список параметром функции.
 * @param fn Функция
 */const getFunctionParams = fn => {
    const COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/gm;
    const DEFAULT_PARAMS = /=[^,]+/gm;
    const FAT_ARROW = /=>.*$/gm;
    const ARGUMENT_NAMES = /([^\s,]+)/g;
    const formattedFn = fn
        .toString()
        .replace(COMMENTS, "")
        .replace(FAT_ARROW, "")
        .replace(DEFAULT_PARAMS, "");
    const params = formattedFn
        .slice(formattedFn.indexOf("(") + 1, formattedFn.indexOf(")"))
        .match(ARGUMENT_NAMES);
    return params || [];
};
const getFullName = (name, surname, middlename) => {
      console.log(surname + ' ' + name + ' ' + middlename);
};
console.log(getFunctionParams(getFullName)); // ["name", "surname", "middlename"]


Getting the body function
/**
 * Получить строковое представление тела функции.
 * @param fn Функция
 */const getFunctionBody = fn => {
    const restoreIndent = body => {
        const lines = body.split("\n");
        const bodyLine = lines.find(line => line.trim() !== "");
        let indent = typeof bodyLine !== "undefined" ? (/[ \t]*/.exec(bodyLine) || [])[0] : "";
        indent = indent || "";
        return lines.map(line => line.replace(indent, "")).join("\n");
    };
    const fnStr = fn.toString();
    const rawBody = fnStr.substring(
        fnStr.indexOf("{") + 1,
        fnStr.lastIndexOf("}")
    );
    const indentedBody = restoreIndent(rawBody);
    const trimmedBody = indentedBody.replace(/^\s+|\s+$/g, "");
    return trimmedBody;
};
// Получим список параметров и тело функции getFullNameconst getFullName = (name, surname, middlename) => {
    console.log(surname + ' ' + name + ' ' + middlename);
};
console.log(getFunctionBody(getFullName));


It is important to note that when using the minifier, both the code itself inside the parsed function and its arguments can be optimized and, therefore, changed.


3. Work with objects


In JavaScript, there is a global Object object containing a number of methods for dynamic work with objects.


Most of these methods from there have long existed in the language and are widely used.


Object properties


  • Object.assign - for easy copying the properties of one or several objects into the object specified by the first parameter:


    Object.assign({}, { a: 1 }, { b: 2 }, { c: 3 }) // {a: 1, b: 2, c: 3}

  • Object.keys and Object.values - returns either a list of keys, or a list of object values:


    const obj = { a: 1, b: 2, c: 3 };
    console.log(Object.keys(obj)); // ["a", "b", "c"]console.log(Object.values(obj)); // [1, 2, 3]

  • Object.entries - returns a list of its properties in the format [[key1, value1], [key2, value2]] :


    const obj = { a: 1, b: 2, c: 3 };
    console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]]

  • Object.prototype.hasOwnProperty - checks if a property is contained in an object (not in its prototype chain):


    const obj = { a: 1 };
    obj.__proto__ = { b: 2 };
    console.log(obj.hasOwnProperty('a')); // trueconsole.log(obj.hasOwnProperty('b')) // false

  • Object.getOwnPropertyNames - returns a list of its own properties, including both enumerable and non-enumerable:


    const obj = { a: 1, b: 2 };
    Object.defineProperty(obj, 'c', { value: 3, enumerable: false }); // Создаём неперечисляемое свойствоfor (let key in obj) {
        console.log(key);
    }
    // "a", "b"console.log(Object.getOwnPropertyNames(obj)); // [ "a", "b", "c" ]

  • Object.getOwnPropertySymbols - returns a list of its own (contained in the object, and not in its prototype chain) characters:


    const obj = {};
    const a = Symbol('a');
    obj[a] = 1;
    console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(a) ]

  • Object.prototype.propertyIsEnumerable - checks if a property is enumerable (for example, is available in for-in, for-of loops):


    const arr = [ 'Первый элемент' ];
    console.log(arr.propertyIsEnumerable(0)); // true — элемент  'Первый элемент' является перечисляемымconsole.log(arr.propertyIsEnumerable('length')); // false — свойство length не является перечисляемым


Object Property Descriptors


Descriptors allow you to fine-tune property settings. Using them, we can conveniently do our own interceptors while reading / writing any property (getters and setters - get / set), make properties immutable or non-enumerable, and a number of other things.


  • Object.defineProperty and Object.defineProperties - creates one or more property descriptors. Create your own descriptor with a getter and a setter:


    const obj = { name: 'Михаил', surname: 'Горшенёв' };
    Object.defineProperty(obj, 'fullname', {
        // Вызывается при чтении свойства fullname
        get: function() { 
            return`${this.name}${this.surname}`;
        },
        // Вызывается при изменении свойства fullname (но не умеет перехватывать удаление delete obj.fullname)
        set: function(value) {
            const [name, surname] = value.split(' ');
            this.name = name;
            this.surname = surname;
        },
    });
    console.log(obj.fullname); // Михаил Горшенёв
    obj.fullname = 'Егор Летов';
    console.log(obj.name); // Егорconsole.log(obj.surname); // Летов

    In the example above, the fullname property did not have its own value, but dynamically worked with the name and surname properties. It is not necessary to define a getter and a setter at the same time — we can leave only the getter and get a read-only property. Or we can add an additional action in the setter along with setting the value, for example, logging.
    In addition to the get / set properties, descriptors have a few more properties to configure:


    const obj = {};
    // Если не нужны свои обработчики get/set, то можно просто указать значение через value. Нельзя одновременно использовать get/set и value. По умолчанию — undefined. Object.defineProperty(obj, 'name', { value: 'Егор' });
    // Указываем, что созданное свойство видно при итерации свойств объекта (for-in, for-of, Object.keys). По умолчанию — false.Object.defineProperty(obj, 'a', { enumerable: true });
    // Можно ли в дальнейшем поменять созданное свойство через defineProperty или удалить его через delete. По умолчанию — false.Object.defineProperty(obj, 'b', { configurable: false });
    // Можно ли будет менять значение свойства. По умолчанию — false.Object.defineProperty(obj, 'c', { writable: true });

  • Object.getOwnPropertyDescriptor and Object.getOwnPropertyDescriptors - allows you to get the desired object handle or a complete list of them:


    const obj = { a: 1, b: 2 };
    console.log(Object.getOwnPropertyDescriptor(obj, "a")); // { configurable: true, enumerable: true, value: 1, writable: true }/**
     * {
     *     a: { configurable: true, enumerable: true, value: 1, writable: true },
     *     b: { configurable: true, enumerable: true, value: 2, writable: true }
     * }
     */console.log(Object.getOwnPropertyDescriptors(obj));


Creating restrictions when working with objects


  • Object.freeze - freezes object properties. The consequence of such a "freeze" is the complete immutability of the properties of the object - they cannot be changed and deleted, add new ones, change descriptors:


    const obj = Object.freeze({ a: 1 });
    // В строгом режиме следующие строчки кидают исключения, а в обычном просто ничего не происходит.
    obj.a = 2; 
    obj.b = 3;
    console.log(obj); // { a: 1 }console.log(Object.isFrozen(obj)) // true

  • Object.seal - "seals" the properties of the object. "Sealing" is similar to Object.freeze, but has a number of differences. We also, as in Object.freeze, prohibit adding new properties, deleting existing properties, changing their descriptors, but at the same time we can change property values:


    const obj = Object.seal({ a: 1 });
    obj.a = 2; // Свойство a теперь равно 2// В строгом режиме кинет исключение, а в обычном просто ничего не происходит.
    obj.b = 3;
    console.log(obj); // { a: 2 }console.log(Object.isSealed(obj)) // true

  • Object.preventExtensions - prohibits adding new properties / descriptors:


    const obj = Object.preventExtensions({ a: 1 });
    obj.a = 2;
    // В строгом режиме следующие строчки кидают исключения, а в обычном просто ничего не происходит.
    obj.b = 3; 
    console.log(obj); // { a: 2 }console.log(Object.isExtensible(obj)) // false


Object Prototypes


  • Object.create - to create an object with the specified prototype in the parameter. This feature can be used both for prototype inheritance and for creating "clean" objects, without properties from the Object.prototype :


    const pureObj = Object.create(null);

  • Object.getPrototypeOf and Object.setPrototypeOf - to get / change the prototype of the object:


    const duck = {};
    const bird = {};
    Object.setPrototypeOf(duck, bird);
    console.log(Object.getPrototypeOf(duck) === bird); // trueconsole.log(duck.__proto__ === bird); // true

  • Object.prototype.isPrototypeOf - checks if the current object is contained in the prototype chain of another:


    const duck = {};
    const bird = {};
    duck.__proto__ = bird;
    console.log(bird.isPrototypeOf(duck)); // true


4. Reflect API


With the advent of ES6, a global object Reflect has been added to JavaScript , designed to store various methods related to reflection and introspection.


Most of its methods are the result of migrating existing methods from global objects such as Object and Function into a separate namespace with little refactoring for more comfortable use.


Transferring functions to the Reflect object not only made it easier to find the right methods for reflection and gave greater semanticism, but also avoided unpleasant situations when our object does not contain Object.prototype in its prototype , but we want to use methods from there:


let obj = Object.create(null);
obj.qwerty = 'qwerty';
console.log(obj.__proto__) // nullconsole.log(obj.hasOwnProperty('qwerty')) // Uncaught TypeError: obj.hasOwnProperty is not a functionconsole.log(obj.hasOwnProperty === undefined); // trueconsole.log(Object.prototype.hasOwnProperty.call(obj, 'qwerty')); // true

Refactoring made the behavior of methods more explicit and uniform. For example, if earlier when an Object.defineProperty was called on an incorrect value (like a number or a string), an exception was thrown, but at the same time, an Object.getOwnPropertyDescriptor call on a non-existing object descriptor silently returned undefined, then similar methods from Reflect always give exceptions when there is incorrect data .


Also added a few new methods:


  • Reflect.construct is a more convenient alternative to Object.create , which allows not only creating an object with the specified prototype, but also immediately initializing it:


    functionPerson(name, surname) {
        this.name = this.formatParam(name);
        this.surname = this.formatParam(surname);
    }
    Person.prototype.formatParam = function(param) {
        return param.slice(0, 1).toUpperCase() + param.slice(1).toLowerCase();
    }
    const oldPerson = Object.create(Person.prototype); // {}
    Person.call(oldPerson, 'Иван', 'Иванов'); // {name: "Иван", surname: "Иванов"}const newPerson = Reflect.construct(Person, ['Андрей', 'Смирнов']); // {name: "Андрей", surname: "Смирнов"}

  • Reflect.ownKeys - returns an array of properties belonging to the specified object (and not objects in the prototype chain):


    let person = { name: 'Иван', surname: 'Иванов' };
    person.__proto__ = { age: 30 };
    console.log(Reflect.ownKeys(person)); // ["name", "surname"]

  • Reflect.deleteProperty is an alternative to the delete operator , implemented as a method:


    let person = { name: 'Иван', surname: 'Иванов' };
    delete person.name; // person = {surname: "Иванов"}Reflect.deleteProperty(person, 'surname'); // person = {}

  • Reflect.has is an alternative to the in operator , implemented as a method:


    let person = { name: 'Иван', surname: 'Иванов' };
    console.log('name'in person); // trueconsole.log(Reflect.has(person, 'name')); // true

  • Reflect.get and Reflect.set - to read / change object properties:


    let person = { name: 'Иван', surname: 'Иванов' };
    console.log(Reflect.get(person, 'name')); // ИванReflect.set(person, 'surname', 'Петров') // person = {name: "Иван", surname: "Петров"}


More changes can be found here .


Reflect metadata


In addition to the Reflect object methods listed above, there is an experimental proposal for easily linking various metadata to objects.


Metadata can be any useful information not directly related to the object, for example:


  • TypeScript with the emitDecoratorMetadata flag enabled writes type information to the metadata, allowing you to access it in runtime. Further, this information can be obtained by key design: type:
    const typeData = Reflect.getMetadata("design:type", object, propertyName);
  • The popular InversifyJS library for inversion of control stores in the metadata various information about the described relationships.

This polyfill is currently used for browsing.


5. Symbols (Symbols)


Symbols are a new immutable data type, mainly used to create unique names for object property identifiers. We have the ability to create characters in two ways:


  1. Local symbols - the text in the parameters of the Symbol function does not affect the uniqueness and is needed only for debugging:


    const sym1 = Symbol('name');
    const sym2 = Symbol('name');
    console.log(sym1 == sym2); // false

  2. Global symbols - symbols are stored in the global registry, therefore symbols with the same key are equal:


    const sym3 = Symbol.for('name');
    const sym4 = Symbol.for('name');
    const sym5 = Symbol.for('other name');
    console.log(sym3 == sym4); // true, символы имеют один и тот же ключ 'name'console.log(sym3 == sym5); // false, символы имеют разные ключи


The ability to create such identifiers allows you to not be afraid that we can wipe a property in an unknown object. This quality allows standard creators to easily add new standard properties to objects without breaking compatibility with various existing libraries (which could already define the same property) and user code. Therefore, there are a number of standard symbols and some of them provide new opportunities for reflection:


  • Symbol.iterator - allows you to create your own rules for iterating objects using for-of or ... spread operator :


    let arr = [1, 2, 3];
    // Выводим элементы массива в обратном порядке
    arr[Symbol.iterator] = function() {
        const self = this;
        let pos = this.length - 1;
        return {
            next() {
                if (pos >= 0) {
                    return {
                        done: false,
                        value: self[pos--]
                    };
                } else {
                    return {
                        done: true
                    };
                }
            }
        }
    };
    console.log([...arr]); // [3, 2, 1]

  • Symbol.hasInstance - a method that determines whether the constructor recognizes an object as its own instance. Used by the instanceof operator:


    classMyArray{  
        static [Symbol.hasInstance](instance) {
            returnArray.isArray(instance);
        }
    }
    console.log([] instanceof MyArray); // true

  • Symbol.isConcatSpreadable - indicates whether the array should flatten during concatenation in Array.concat:


    let firstArr = [1, 2, 3];
    let secondArr = [4, 5, 6];
    firstArr.concat(secondArr); // [1, 2, 3, 4, 5, 6]
    secondArr[Symbol.isConcatSpreadable] = false;
    console.log(firstArr.concat(secondArr)); // [1, 2, 3, [4, 5, 6]]

  • Symbol.species - allows you to specify which constructor will be used to create derived objects inside the class.
    For example, we have a standard Array class for working with arrays and it has a .map method that creates a new array based on the current one. To find out which class to use to create this new array, Array refers to this.constructor [Symbol.species] like this:


    Array.prototype.map = function(cb) {
        const ArrayClass = this.constructor[Symbol.species];
        const result = new ArrayClass(this.length);
        this.forEach((value, index, arr) => {
            result[index] = cb(value, index, arr);
        });
        return result;
    }

    Thus, by redefining Symbol.species, we can create our own class for working with arrays and say that all standard methods like .map, .reduce, etc. return not an instance of the Array class, but an instance of our class:


    classMyArrayextendsArray{
        static get [Symbol.species]() { returnthis; }
    }
    const arr = new MyArray(1, 2, 3); // [1, 2, 3]console.log(arr instanceof MyArray); // trueconsole.log(arr instanceofArray); // true// Обычная реализация Array.map вернула бы экземпляр класса Array, но мы переопределили Symbol.species на this и теперь возвращается экземпляр класса MyArrayconst doubledArr = arr.map(x => x * 2);
    console.log(doubledArr instanceof MyArray); // trueconsole.log(doubledArr instanceofArray); // true

    Of course, this works not only with arrays, but also with other standard classes. Moreover, even if we simply create our own class with methods that return new instances of the same class, we should use this.constructor [Symbol.species] to get the reference to the designer in a good way.


  • Symbol.toPrimitive - allows you to specify how to convert our object into a primitive value. If earlier we had to use toString together with valueOf to bring it to a primitive, now everything can be done in one convenient method:


    const figure = {
        id: 1,
        name: 'Прямоугольник',
        [Symbol.toPrimitive](hint) {
            if (hint === 'string') {
                returnthis.name;
            } elseif (hint === 'number') {
                returnthis.id;
            } else {  // defaultreturnthis.name;
            }
        }
    }
    console.log(`${figure}`); // hint = stringconsole.log(+figure); // hint = numberconsole.log(figure + ''); // hint = default

  • Symbol.match - allows you to create your own handler classes for the method for the String.prototype.match function :


    classStartAndEndsWithMatcher{
        constructor(value) {
            this.value = value;
        }
        [Symbol.match](str) {
            const startsWith = str.startsWith(this.value);
            const endsWith = str.endsWith(this.value);
            if (startsWith && endsWith) {
                return [this.value];
            }
            returnnull;
        }
    }
    const testMatchResult = '|тест|'.match(new StartAndEndsWithMatcher('|'));
    console.log(testMatchResult); // ["|"]const catMatchResult = 'кот|'.match(new StartAndEndsWithMatcher('|'));
    console.log(catMatchResult) // null

    There are also similar symbols - Symbol.replace , Symbol.search and Symbol.split for similar methods from String.prototype .



It is important to note that symbols (like reflect-metadata from the previous section) can be used to attach their metadata to any object. After all, because of the uniqueness of the created characters, we can not be afraid that we accidentally overwrite the existing property in the object. For example, attach metadata for validation to an object:


const validationRules = Symbol('validationRules');
const person = { name: 'Иван', surname: 'Иванов' };
person[validationRules] = {
    name: ['max-length-256', 'required'],
    surname: ['max-length-256']
};

6. Proxy (Proxy)


Proxy is a fundamentally new functionality that appeared along with Reflect API and Symbols in ES6, designed to intercept any properties in read / write / delete, call functions, override iteration rules and other useful things. It is important to note that the proxy does not normally polish .


Using proxies, we can greatly extend the usability of heaps of libraries, for example, libraries for data-binding, such as MobX from React, Vue, and others. Consider an example before and after using a proxy.


With proxy:


const formData = {
    login: 'User',
    password: 'pass'
};
const proxyFormData = newProxy(formData, {
    set(target, name, value) {
        target[name] = value;
        this.forceUpdate(); // Перерисовываем наш React-компонент
    }
});
// При изменении любого свойства также вызывается forceUpdate() для перерисовки в React
proxyFormData.login = 'User2';
// Такого свойства ещё не существует, но прокси всё-равно перехватит присваивание и корректно обработает
proxyFormData.age = 20; 

Without a proxy, we can try to make some kind of similar using getters / setters:


const formData = {
    login: 'User',
    password: 'pass'
};
const proxyFormData = {};
for (let param in formData) {
    Reflect.defineProperty(proxyFormData, `__private__${param}`, {
        value: formData[param],
        enumerable: false,
        configurable: true
    }); 
    Reflect.defineProperty(proxyFormData, param, {
        get: function() { 
            returnthis[`__private__${param}`];
        },
        set: function(value) {
            this[`__private__${param}`] = value;
            this.forceUpdate(); // Перерисовываем наш React-компонент
        },
        enumerable: true,
        configurable: true
    }); 
}
// При изменении любого свойства также вызывается forceUpdate() для перерисовки в React
proxyFormData.login = 'User2';
// Такого свойства не существует и мы не сможем его обработать пока явно не зададим ещё одну пара геттеров-сеттеров через Reflect.defineProperty
proxyFormData.age = 20; 

When using getters and setters, we get a bunch of uncomfortable boilerplate code, and the main disadvantage is that when using Proxy, we create a proxied object once and it intercepts all properties (regardless of whether they exist in the object or not), and using getters / setters we have to manually create a pair from the getter and the setter for each new property, besides the setter we cannot track the operation of the delete obj [name] operator .


7. Conclusion


JavaScript is a powerful and flexible language and is very pleased that it is no longer stagnant as in the times of ECMAScript 4, but is constantly improving and adding more and more new and convenient features. Due to this, we can write more and more good programs both in terms of user experience and development.


For a more detailed immersion in the topic, I recommend reading the freely available section on metaprogramming of the great book You Don't Know JS .


Also popular now: