⬆️ ⬇️

JavaScript Metaprogramming

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:



 <?php class Comment { public $authorId; public $comment; public function __construct($authorId, $comment) { $this->authorId = $authorId; $this->comment = $comment; } //       public function __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:





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



 const hello = new Function('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:





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; }; //       getFullName const 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 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.





Creating restrictions when working with objects





Object Prototypes





4. Reflect API



With the advent of ES6, a global Reflect object 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__) // null console.log(obj.hasOwnProperty('qwerty')) // Uncaught TypeError: obj.hasOwnProperty is not a function console.log(obj.hasOwnProperty === undefined); // true console.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:





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:





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 object unknown to us. 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 custom code. Therefore, there are a number of standard symbols and some of them provide new opportunities for reflection:





, ( reflect-metadata ) . - , , . :



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


6. (Proxy)



Proxy , Reflect API Symbols ES6, // , , . , .



, data-binding MobX React, Vue . .



:



 const formData = { login: 'User', password: 'pass' }; const proxyFormData = new Proxy(formData, { set(target, name, value) { target[name] = value; this.forceUpdate(); //   React- } }); //       forceUpdate()    React proxyFormData.login = 'User2'; //     ,   -      proxyFormData.age = 20; 


, /:



 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() { return this[`__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; 


-, — Proxy ( , ), / , delete obj[name] .



7.



JavaScript , ECMAScript 4, . , .



You Don't Know JS .



')

Source: https://habr.com/ru/post/417097/



All Articles