La métaprogrammation est un type de programmation associé à la création de programmes qui génèrent d'autres programmes à la suite de leur travail, ou des programmes qui se modifient pendant l'exécution. (Wikipedia)
Dans un langage plus simple, la métaprogrammation dans JavaScript peut être considérée comme un mécanisme qui vous permet d'analyser et de modifier le programme en temps réel en fonction de toute action. Et, très probablement, vous les utilisez d'une manière ou d'une autre pour écrire des scripts tous les jours.
JavaScript, de par sa nature, est un langage dynamique très puissant et vous permet d'écrire facilement du code flexible:
const comment = { authorId: 1, comment: '' }; for (let name in comment) { const pascalCasedName = name.slice(0, 1).toUpperCase() + name.slice(1); comment[`save${pascalCasedName}`] = function() {
Un code similaire pour la création dynamique de méthodes dans d'autres langues peut très souvent nécessiter une syntaxe ou une API spéciale pour cela. Par exemple, PHP est également un langage dynamique, mais il nécessitera plus d'efforts:
<?php class Comment { public $authorId; public $comment; public function __construct($authorId, $comment) { $this->authorId = $authorId; $this->comment = $comment; }
En plus de la syntaxe flexible, nous avons également un tas de fonctions utiles pour écrire du code dynamique: Object.create, Object.defineProperty, Function.apply et bien d'autres.
Considérez-les plus en détail.
- Génération de code
- Travailler avec des fonctions
- Travailler avec des objets
- API Reflect
- Symboles
- Proxy
- Conclusion
1. Génération de code
L'outil standard pour exécuter dynamiquement du code est la fonction eval , qui vous permet d'exécuter du code à partir de la chaîne passée:
eval('alert("Hello, world")');
Malheureusement, eval a de nombreuses nuances:
- si notre code est écrit en mode strict ('use strict'), alors les variables déclarées dans eval ne seront pas visibles dans le code eval appelant. En même temps, le code lui-même dans eval peut toujours changer les variables externes.
- le code dans eval peut être exécuté à la fois dans le contexte global (s'il est appelé via window.eval) et dans le contexte de la fonction à l'intérieur de laquelle l'appel a été effectué (si juste eval, sans fenêtre).
- des problèmes peuvent survenir en raison de la minification JS, lorsque les noms de variables sont remplacés par des noms plus courts pour réduire la taille. Le code transmis sous forme de chaîne à eval ne touche généralement pas le minifieur, c'est pourquoi nous pouvons commencer à accéder à des variables externes en utilisant d'anciens noms non minimisés, ce qui entraînera des erreurs subtiles.
Il existe une excellente alternative à la résolution de ces problèmes: la nouvelle fonction .
const hello = new Function('name', 'alert("Hello, " + name)'); hello('')
Contrairement à eval, nous pouvons toujours passer explicitement les paramètres à travers les arguments d'une fonction et lui donner dynamiquement le contexte de celle-ci (via Function.apply ou Function.call ). De plus, la fonction créée est toujours appelée dans la portée globale.
Autrefois, eval était souvent utilisé pour changer dynamiquement le code, comme JavaScript avait très peu de mécanismes de réflexion et il était impossible de se passer d'eval. Mais dans la norme de langage moderne, beaucoup plus de fonctionnalités de haut niveau sont apparues et eval est maintenant utilisé beaucoup moins souvent.
2. Travailler avec des fonctions
JavaScript nous fournit de nombreux excellents outils pour travailler dynamiquement avec les fonctions, nous permettant à la fois d'obtenir diverses informations sur la fonction lors de l'exécution et de la modifier:
Function.length - vous permet de connaître le nombre d'arguments d'une fonction:
const func = function(name, surname) { console.log(`Hello, ${surname} ${name}`) }; console.log(func.length)
Function.apply et Function.call - vous permettent de changer dynamiquement le contexte de cette fonction:
const person = { name: '', introduce: function() { return ` ${this.name}`; } } person.introduce();
Ils ne diffèrent les uns des autres que par le fait que, dans Function.apply, les arguments de la fonction sont servis sous forme de tableau et dans Function.call, séparés par des virgules. Cette fonctionnalité était souvent utilisée auparavant pour transmettre une liste d'arguments à la fonction sous forme de tableau. Un exemple courant est la fonction Math.max (par défaut, elle ne peut pas fonctionner avec les tableaux):
Math.max.apply(null, [1, 2, 4, 3]);
Avec l'avènement du nouvel opérateur de spread, vous pouvez simplement écrire ceci:
Math.max(...[1, 2, 4, 3]);
Function.bind - vous permet de créer une copie d'une fonction à partir d'une fonction existante, mais avec un contexte différent:
const person = { name: '', introduce: function() { return ` ${this.name}`; } } person.introduce();
Function.caller - vous permet d'obtenir la fonction appelante. Il n'est pas recommandé de l'utiliser , car il est absent de la norme de langue et ne fonctionnera pas en mode strict. Cela était dû au fait que si divers moteurs JavaScript implémentent l'optimisation d' appel de queue décrite dans la spécification de langage, alors appeler Function.caller peut commencer à produire des résultats incorrects. Exemple d'utilisation:
const a = function() { console.log(a.caller == b); } const b = function() { a(); } b();
Function.toString - Renvoie une représentation sous forme de chaîne de la fonction. Il s'agit d'une fonctionnalité très puissante qui vous permet d'examiner à la fois le contenu d'une fonction et ses arguments:
const getFullName = (name, surname, middlename) => { console.log(`${surname} ${name} ${middlename}`); } getFullName.toString()
Après avoir reçu une représentation sous forme de chaîne d'une fonction, nous pouvons l'analyser et l'analyser. Cela peut être utilisé, par exemple, pour extraire les noms des arguments de fonction et, selon le nom, remplacer automatiquement le paramètre souhaité. En général, il existe deux façons d'analyser:
- Analyser un tas d'habitués et nous obtenons un niveau de fiabilité acceptable (peut ne pas fonctionner si nous ne couvrons pas tous les types possibles d'entrées de fonction).
- Nous obtenons la représentation sous forme de chaîne de la fonction et la mettons dans l'analyseur JavaScript fini (par exemple, esprima ou gland ), puis nous travaillons avec l'AST structuré. Exemple d'analyse AST via esprima. Je peux également conseiller un bon rapport sur les analyseurs d'Alexei Okhrimenko.
Exemples simples avec analyse régulière des fonctions:
Obtention d'une liste d'arguments de fonction 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));
Obtenir la fonction corporelle 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));
Il est important de noter que lorsque vous utilisez le minifieur, le code lui-même à l'intérieur de la fonction analysée et ses arguments peuvent être optimisés et, par conséquent, modifiés.
3. Travailler avec des objets
JavaScript a un objet Object global qui contient de nombreuses méthodes pour travailler dynamiquement avec des objets.
La plupart de ces méthodes existent depuis longtemps dans la langue et sont largement utilisées.
Propriétés des objets
Object.assign - pour copier facilement les propriétés d'un ou plusieurs objets vers l'objet spécifié par le premier paramètre:
Object.assign({}, { a: 1 }, { b: 2 }, { c: 3 })
Object.keys et Object.values - renvoie soit une liste de clés, soit une liste de valeurs d'objets:
const obj = { a: 1, b: 2, c: 3 }; console.log(Object.keys(obj));
Object.entries - retourne une liste de ses propriétés au format [[key1, value1], [key2, value2]] :
const obj = { a: 1, b: 2, c: 3 }; console.log(Object.entries(obj));
Object.prototype.hasOwnProperty - Vérifie si une propriété est contenue dans un objet (pas dans sa chaîne de prototype):
const obj = { a: 1 }; obj.__proto__ = { b: 2 }; console.log(obj.hasOwnProperty('a'));
Object.getOwnPropertyNames - renvoie une liste de ses propres propriétés, y compris à la fois énumérées et non énumérées:
const obj = { a: 1, b: 2 }; Object.defineProperty(obj, 'c', { value: 3, enumerable: false });
Object.getOwnPropertySymbols - renvoie une liste de ses propres caractères (contenus dans l'objet, et non dans sa chaîne prototype):
const obj = {}; const a = Symbol('a'); obj[a] = 1; console.log(Object.getOwnPropertySymbols(obj));
Object.prototype.propertyIsEnumerable - vérifie si une propriété est énumérable (par exemple, est disponible dans les boucles for-in, for-of):
const arr = [ ' ' ]; console.log(arr.propertyIsEnumerable(0));
Descripteurs de propriétés d'objet
Les descripteurs vous permettent d'affiner les paramètres de propriété. En les utilisant, nous pouvons facilement créer nos propres intercepteurs lors de la lecture / écriture de toute propriété (getters et setters - get / set), rendre les propriétés immuables ou non énumérables, et un certain nombre d'autres choses.
Object.defineProperty et Object.defineProperties - crée un ou plusieurs descripteurs de propriété. Créez votre propre descripteur avec getter et setter:
const obj = { name: '', surname: '' }; Object.defineProperty(obj, 'fullname', {
Dans l'exemple ci-dessus, la propriété fullname n'avait pas sa propre valeur, mais fonctionnait dynamiquement avec les propriétés name et patronyme. Il n'est pas nécessaire de définir à la fois un getter et un setter - nous ne pouvons laisser que le getter et obtenir une propriété en lecture seule. Ou nous pouvons ajouter une action supplémentaire dans le setter avec la définition de la valeur, par exemple, la journalisation.
En plus des propriétés get / set, les descripteurs ont plusieurs autres propriétés à configurer:
const obj = {};
Object.getOwnPropertyDescriptor et Object.getOwnPropertyDescriptors - vous permettent d'obtenir le descripteur d'objet souhaité ou leur liste complète:
const obj = { a: 1, b: 2 }; console.log(Object.getOwnPropertyDescriptor(obj, "a"));
Création de restrictions lors de l'utilisation d'objets
Object.freeze - "fige" les propriétés d'un objet. La conséquence d'un tel «gel» est l'immuabilité complète des propriétés de l'objet - elles ne peuvent pas être modifiées et supprimées, de nouvelles ajoutées, les descripteurs modifiés:
const obj = Object.freeze({ a: 1 });
Object.seal - "scelle" les propriétés d'un objet. L'étanchéité est similaire à Object.freeze, mais présente un certain nombre de différences. Nous, comme dans Object.freeze, interdisons l'ajout de nouvelles propriétés, la suppression de celles existantes, la modification de leurs descripteurs, mais en même temps, nous pouvons modifier les valeurs des propriétés:
const obj = Object.seal({ a: 1 }); obj.a = 2;
Object.preventExtensions - interdit l'ajout de nouvelles propriétés / descripteurs:
const obj = Object.preventExtensions({ a: 1 }); obj.a = 2;
Prototypes d'objets
Object.create - pour créer un objet avec le prototype spécifié dans le paramètre. Cette fonctionnalité peut être utilisée à la fois pour l' héritage de prototypes et pour créer des objets "propres", sans propriétés d' Object.prototype :
const pureObj = Object.create(null);
Object.getPrototypeOf et Object.setPrototypeOf - pour obtenir / modifier le prototype d'un objet:
const duck = {}; const bird = {}; Object.setPrototypeOf(duck, bird); console.log(Object.getPrototypeOf(duck) === bird);
Object.prototype.isPrototypeOf - Vérifie si l'objet actuel est contenu dans la chaîne de prototype d'un autre:
const duck = {}; const bird = {}; duck.__proto__ = bird; console.log(bird.isPrototypeOf(duck));
4. Reflect API
Avec l'avènement d'ES6, un objet global Reflect a été ajouté à JavaScript pour stocker diverses méthodes liées à la réflexion et à l'introspection.
La plupart de ses méthodes sont le résultat du transfert de méthodes existantes d'objets globaux tels que Object et Function vers un espace de noms séparé avec un peu de refactoring pour une utilisation plus confortable.
Le transfert de fonctions vers l'objet Reflect a non seulement facilité la recherche des méthodes de réflexion nécessaires et a donné une plus grande sémantique, mais a également évité les situations désagréables lorsque notre objet ne contient pas Object.prototype dans son prototype, mais nous voulons utiliser les méthodes à partir de là:
let obj = Object.create(null); obj.qwerty = 'qwerty'; console.log(obj.__proto__)
La refactorisation a rendu le comportement des méthodes plus explicite et monotone. Par exemple, si auparavant, lors de l'appel d' Object.defineProperty sur une valeur incorrecte (comme un nombre ou une chaîne), une exception était levée, mais en même temps, l'appel d' Object.getOwnPropertyDescriptor sur un descripteur d'objet inexistant retournait silencieusement indéfini, alors les méthodes similaires de Reflect lèvent toujours des exceptions pour les données incorrectes .
Plusieurs nouvelles méthodes ont également été ajoutées:
Reflect.construct est une alternative plus pratique à Object.create , qui permet non seulement de créer un objet avec le prototype spécifié, mais aussi de l'initialiser immédiatement:
function Person(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);
Reflect.ownKeys - renvoie un tableau de propriétés appartenant à l'objet spécifié (et non aux objets de la chaîne de prototype):
let person = { name: '', surname: '' }; person.__proto__ = { age: 30 }; console.log(Reflect.ownKeys(person));
Reflect.deleteProperty - une alternative à l'opérateur de suppression , faite sous la forme d'une méthode:
let person = { name: '', surname: '' }; delete person.name;
Reflect.has - une alternative à l'opérateur in , faite sous la forme d'une méthode:
let person = { name: '', surname: '' }; console.log('name' in person);
Reflect.get et Reflect.set - pour lire / modifier les propriétés des objets:
let person = { name: '', surname: '' }; console.log(Reflect.get(person, 'name'));
Plus de détails sur les changements peuvent être trouvés ici .
En plus des méthodes d'objet Reflect répertoriées ci-dessus, il existe une proposition expérimentale pour lier facilement diverses métadonnées à des objets.
Les métadonnées peuvent être toute information utile qui n'est pas directement liée à l'objet, par exemple:
À l'heure actuelle, ce polyfill est utilisé pour fonctionner dans les navigateurs .
5. Symboles
Les symboles sont un nouveau type de données immuable, principalement utilisé pour créer des noms uniques pour les identificateurs de propriété d'objet. Nous avons la possibilité de créer des personnages de deux manières:
Symboles locaux - le texte des paramètres de la fonction Symbole n'affecte pas l'unicité et n'est nécessaire que pour le débogage:
const sym1 = Symbol('name'); const sym2 = Symbol('name'); console.log(sym1 == sym2);
Caractères globaux - les caractères sont stockés dans le registre global, donc les caractères avec la même clé sont égaux:
const sym3 = Symbol.for('name'); const sym4 = Symbol.for('name'); const sym5 = Symbol.for('other name'); console.log(sym3 == sym4);
La possibilité de créer de tels identifiants nous permet de ne pas avoir peur de remplacer une propriété dans un objet qui nous est inconnu. Cette qualité permet aux créateurs de la norme d'ajouter facilement de nouvelles propriétés standard aux objets, sans rompre la compatibilité avec diverses bibliothèques existantes (qui pourraient déjà définir la même propriété) et le code utilisateur. Par conséquent, il existe un certain nombre de symboles standard et certains d'entre eux offrent de nouvelles opportunités de réflexion:
Symbol.iterator - vous permet de créer vos propres règles pour itérer des objets en utilisant l' opérateur for-of ou ... spread :
let arr = [1, 2, 3];
Symbol.hasInstance est une méthode qui détermine si un constructeur reconnaît un objet comme son instance. Utilisé par l'opérateur instanceof:
class MyArray { static [Symbol.hasInstance](instance) { return Array.isArray(instance); } } console.log([] instanceof MyArray);
Symbol.isConcatSpreadable - Indique si le tableau doit s'aplatir lorsqu'il est concaténé dans Array.concat:
let firstArr = [1, 2, 3]; let secondArr = [4, 5, 6]; firstArr.concat(secondArr);
Symbol.species - vous permet de spécifier quel constructeur sera utilisé pour créer des objets dérivés à l'intérieur de la classe.
Par exemple, nous avons une classe Array standard pour travailler avec des tableaux et elle a une méthode .map qui crée un nouveau tableau basé sur celui actuel. Pour savoir quelle classe utiliser pour créer ce nouveau tableau, Array appelle this.constructor [Symbol.species] comme ceci:
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; }
Ainsi, en remplaçant Symbol.species, nous pouvons créer notre propre classe pour travailler avec des tableaux et dire que toutes les méthodes standard comme .map, .reduce, etc. ne renvoient pas une instance de la classe Array, mais une instance de notre classe:
class MyArray extends Array { static get [Symbol.species]() { return this; } } const arr = new MyArray(1, 2, 3);
Bien sûr, cela fonctionne non seulement avec les tableaux, mais aussi avec d'autres classes standard. De plus, même si nous créons simplement notre propre classe avec des méthodes qui retournent de nouvelles instances de la même classe, nous devons utiliser this.constructor [Symbol.species] pour obtenir une référence au constructeur.
Symbol.toPrimitive - vous permet de spécifier comment convertir notre objet en une valeur primitive. Si auparavant, pour réduire à une primitive, nous devions utiliser toString avec valueOf, maintenant tout peut être fait dans une méthode pratique:
const figure = { id: 1, name: '', [Symbol.toPrimitive](hint) { if (hint === 'string') { return this.name; } else if (hint === 'number') { return this.id; } else {
Symbol.match - vous permet de créer vos propres classes de gestionnaire pour la méthode de la fonction String.prototype.match :
class StartAndEndsWithMatcher { 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]; } return null; } } const testMatchResult = '||'.match(new StartAndEndsWithMatcher('|')); console.log(testMatchResult);
— Symbol.replace , Symbol.search Symbol.split String.prototype .
, ( 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();
, /:
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();
-, — Proxy ( , ), / , delete obj[name] .
7.
JavaScript , ECMAScript 4, . , .
You Don't Know JS .