Apprenez à connaître la métaprogrammation en JavaScript

Cet article est partagé par la communauté Huawei Cloud « Métaprogrammation pour rendre le code plus descriptif, expressif et flexible » par Ye Yiyi.

arrière-plan

Au cours du second semestre de l'année dernière, j'ai ajouté de nombreux livres techniques à ma bibliothèque WeChat dans diverses catégories et j'en ai lu certains par intermittence.

Lire sans plan donnera peu de résultats.

Alors que la nouvelle année commence, je suis prêt à essayer autre chose, comme une semaine de lecture. Prévoyez 1 à 2 semaines non consécutives chaque mois pour lire un livre dans son intégralité.

Bien que cette « méthode ludique » soit courante et rigide, elle est efficace, je la lis depuis trois mois.

Il y a deux projets de lecture pour avril et la série "JavaScript You Don't Know" est terminée.

 

Livres lus : "La voie vers une architecture simple", "Node.js d'une manière simple et facile", "JavaScript que vous ne connaissez pas (Volume 1)", "JavaScript que vous ne connaissez pas (Volume 2) ".

 

 

Livre de la Semaine de lecture actuelle : "JavaScript que vous ne connaissez pas (Volume 2)".

 

métaprogrammation

nom de la fonction

Il existe de nombreuses façons d'exprimer une fonction dans un programme, et il n'est pas toujours clair quel devrait être le « nom » de la fonction.

Plus important encore, nous devons déterminer si le « nom » de la fonction est simplement son attribut name (oui, les fonctions ont un attribut appelé name), ou s'il pointe vers son nom lexicalement lié, tel que function bar(){ bar. dans .}.

L'attribut name est utilisé à des fins de métaprogrammation.

Par défaut, le nom lexical de la fonction (le cas échéant) est également défini sur son attribut name. En fait, la spécification ES5 (et précédentes) n'a aucune exigence formelle pour ce comportement. Le paramétrage de l'attribut name n'est pas standard, mais reste relativement fiable. Cela a été standardisé dans ES6.

Dans ES6, il existe désormais un ensemble de règles de dérivation qui peuvent raisonnablement attribuer une valeur à l'attribut name d'une fonction, même si la fonction n'a pas de nom lexical disponible.

Par exemple:

var abc = fonction () {
  // ..
} ;

abc.nom; // "abc"

Voici quelques autres formes de dérivation de nom (ou d’absence de dérivation) dans ES6 :

(fonction(){ .. }); // nom:
(fonction*(){ .. }); // nom:
window.foo = function(){ .. }; // nom:
classe Génial {
    constructor() { .. } // nom : Génial
    drôle() { .. } // nom : drôle
}

var c = classe Génial { .. } ; // nom : Génial
var o = {
    foo() { .. }, // nom : foo
    *bar() { .. }, // nom : barre
    baz : () => { .. }, // nom : baz
    bam : fonction(){ .. }, // nom : bam
    obtenir quoi() { .. }, // nom : obtenir quoi
    set fuz() { .. }, // nom : set fuz
    ["b" + "iz"] :
      function(){ .. }, // nom : biz
    [Symbole( "buz" )] :
      function(){ .. } // nom : [buz]
} ;

var x = o.foo.bind(o); // nom : lié foo
(function(){ .. }).bind( o ); // nom : lié
export default function() { .. } // nom : par défaut
var y = nouvelle fonction (); // nom : anonyme
où GénérateurFonction =
    fonction*(){}. proto .constructeur ;
var z = new GeneratorFunction(); // nom : anonyme

Par défaut, la propriété name n'est pas accessible en écriture, mais elle est configurable, ce qui signifie qu'elle peut être modifiée manuellement à l'aide de Object.defineProperty(..) si nécessaire.

attribut méta

Les méta-attributs fournissent des méta-informations spéciales sous la forme d'un accès aux attributs qui ne peuvent pas être obtenus par d'autres méthodes.

En prenant new.target comme exemple, le mot-clé new est utilisé comme contexte pour l'accès aux attributs. Évidemment, new lui-même n’est pas un objet, cette fonction est donc très spéciale. Lorsque new.target est utilisé dans un appel de constructeur (une fonction/méthode déclenchée par new), new devient un contexte virtuel, permettant à new.target de pointer vers le constructeur cible qui appelle new.

Il s'agit d'un exemple clair d'opération de métaprogrammation, car son but est de déterminer depuis l'appel du constructeur quelle était la nouvelle cible d'origine, en général pour l'introspection (vérification du type/structure) ou l'accès aux propriétés statiques.

Par exemple, vous souhaiterez peut-être effectuer différentes actions à l'intérieur d'un constructeur selon qu'il est appelé directement ou via une sous-classe :

classe Parent {
  constructeur() {
    if (new.target === Parent) {
      console.log('Parent instancié');
    } autre {
      console.log('Un enfant instancié');
    }
  }
}

la classe Enfant étend Parent {}

var a = nouveau Parent();
// Parent instancié

var b = nouveau Enfant();
// Un enfant instancié

Le constructeur() à l'intérieur de la définition de la classe Parent reçoit en fait le nom lexical de la classe (Parent), même si la syntaxe implique que la classe est une entité distincte du constructeur.

symbole public

JavaScript prédéfinit certains symboles intégrés appelés symboles publics (Well-Known Symbol, WKS).

Ces symboles sont définis principalement pour fournir des méta-propriétés spécialisées afin que ces méta-propriétés puissent être exposées aux programmes JavaScript afin d'obtenir plus de contrôle sur le comportement de JavaScript.

Symbole.iterator

Symbol.iterator représente un emplacement spécial (attribut) sur n'importe quel objet. Le mécanisme du langage trouve automatiquement une méthode à cet emplacement. Cette méthode construit un itérateur pour consommer la valeur de cet objet. De nombreuses définitions d'objet ont une valeur par défaut pour ce symbole.

Cependant, vous pouvez également définir votre propre logique d'itérateur pour des valeurs d'objet arbitraires en définissant la propriété Symbol.iterator, même si celle-ci remplace l'itérateur par défaut. L'aspect métaprogrammation ici est que nous définissons un attribut comportemental qui peut être utilisé par d'autres parties de JavaScript (c'est-à-dire les opérateurs et les constructions de boucles) lorsqu'il s'agit de l'objet défini.

Par exemple:

var cicatrice = [4, 5, 6, 7, 8, 9] ;

pour (var v de arr) {
  console.log(v);
}
// 4 5 6 7 8 9

// Définir un itérateur qui ne produit que des valeurs aux valeurs d'index impaires
arr[Symbol.iterator] = fonction* () {
  où idx = 1 ;
  faire {
    donne ceci[idx];
  } while ((idx += 2) < this.length);
} ;

pour (var v de arr) {
  console.log(v);
}
// 5 7 9

Symbol.toStringTag et Symbol.hasInstance

L’une des tâches de métaprogrammation les plus courantes consiste à introspecter une valeur pour découvrir de quel type il s’agit, généralement pour déterminer quelles opérations sont appropriées à effectuer sur elle. Pour les objets, les techniques d'introspection les plus couramment utilisées sont toString() et instanceof.

Dans ES6, vous pouvez contrôler le comportement de ces opérations :

fonction Foo (salutation) {
  this.greeting = salutation ;
}

Foo.prototype[Symbol.toStringTag] = 'Foo';

Object.defineProperty(Foo, Symbol.hasInstance, {
  valeur : fonction (inst) {
    return inst.greeting == 'bonjour';
  },
});

var a = new Foo('bonjour'),
  b = new Foo('monde');

b[Symbol.toStringTag] = 'cool';

a.toString(); // [objet Foo]
Chaîne(b); // [objet cool]
une instance de Foo ; // vrai

b instance de Foo ; // FAUX

La notation @@toStringTag du prototype (ou de l'instance elle-même) spécifie la valeur de chaîne utilisée lorsque [objet] est chaîne.

La notation @@hasInstance est une méthode sur la fonction constructeur qui accepte une valeur d'objet d'instance et renvoie vrai ou faux pour indiquer si la valeur peut être considérée comme une instance.

Symbole.espèce

Quel constructeur utiliser (Array(..) ou une sous-classe personnalisée) lors de la création d'une sous-classe de Array et que vous souhaitez définir des méthodes héritées (telles que slice(..)). Par défaut, l'appel de slice(..) sur une instance d'une sous-classe Array crée une nouvelle instance de cette sous-classe.

Cette exigence peut être métaprogrammée en remplaçant la définition @@species par défaut d'une classe :

classe cool {
  // Diffère @@espèce aux sous-classes
  statique get [Symbol.species]() {
    rends ceci ;
  }

  encore() {
    return new this.constructor[Symbol.species]();
  }
}

la classe Fun étend Cool {}

la classe Awesome étend Cool {
  //Force que @@species soit spécifié comme constructeur parent
  statique get [Symbol.species]() {
    retourner au frais ;
  }
}

var a = nouveau Fun(),
  b = nouveau Génial(),
  c = a.again(),
  d = b.again();

c instance de Fun ; // vrai
d instance de Awesome ; // FAUX
d instance de Cool ; // vrai

Le comportement par défaut de Symbol.species sur les constructeurs natifs intégrés est de renvoyer ceci. Il n'y a pas de valeur par défaut sur la classe d'utilisateurs, mais comme indiqué, cette fonctionnalité comportementale est facile à simuler.

Si vous devez définir une méthode pour générer de nouvelles instances, utilisez la nouvelle métaprogrammation de modèle this.constructor[Symbol.species](..) au lieu de coder en dur new this.constructor(..) ou new XYZ(..). Les classes héritantes peuvent ensuite personnaliser Symbol.species pour contrôler quel constructeur génère ces instances.

agissant

L’une des nouvelles fonctionnalités de métaprogrammation les plus évidentes d’ES6 est la fonctionnalité Proxy.

Un proxy est un objet spécial que vous créez et qui « encapsule » un autre objet ordinaire – ou se place devant cet objet ordinaire. Vous pouvez enregistrer une fonction de traitement spéciale (c'est-à-dire un piège) sur l'objet proxy. Ce programme sera appelé lors de l'exécution de diverses opérations sur le proxy. Ces gestionnaires ont la possibilité d'effectuer une logique supplémentaire en plus des opérations de transfert vers l'objet cible/encapsulé d'origine.

Un exemple de fonction de gestionnaire d'interruptions que vous pouvez définir sur un proxy est get, qui intercepte l'opération [[Get]] lorsque vous essayez d'accéder aux propriétés d'un objet.

var obj = { une : 1 },
  gestionnaires = {
    get(cible, clé, contexte) {
      // Remarque : cible === obj,
      // contexte === pobj
      console.log('accès : ', clé);
      return Reflect.get (cible, clé, contexte);
    },
  },
  pobj = new Proxy(obj, gestionnaires);

obj.a;
// 1
pobj.a;
// accès à : un
// 1

Nous déclarons une méthode de dénomination de fonction de traitement get(..) sur les gestionnaires (le deuxième paramètre de l'objet Proxy(..)), qui accepte une référence d'objet cible (obj), un nom d'attribut clé ("a") et des littéraux de corps et soi/récepteur/agent (pobj).

Limites de l'agence

Un large éventail d’opérations de base pouvant être effectuées sur des objets peuvent être gérées via ces pièges de fonctions de métaprogrammation. Mais certaines opérations ne peuvent pas (du moins pour l’instant) être interceptées.

var obj = { a:1, b:2 },
gestionnaires = { .. },
pobj = new Proxy( obj, gestionnaires );
type d'objet;
Chaîne( obj );

obj + "" ;
obj == pobj;
obj === pobj

Résumer

Résumons le contenu principal de cet article :

  • Avant ES6, JavaScript disposait déjà de nombreuses fonctions de métaprogrammation, et ES6 fournit plusieurs nouvelles fonctionnalités qui améliorent considérablement les capacités de métaprogrammation.
  • De la dérivation du nom de fonction pour les fonctions anonymes aux métapropriétés qui fournissent des informations sur la façon dont un constructeur est appelé, vous pouvez approfondir plus que jamais la structure de l'exécution de votre programme. En exposant des symboles, vous pouvez remplacer les fonctionnalités d'origine, telles que la conversion de types d'objets en types natifs. Les proxys peuvent intercepter et personnaliser diverses opérations sous-jacentes des objets, et Reflect fournit des outils pour les simuler.
  • L'auteur original recommande : Tout d'abord, concentrez-vous sur la compréhension du fonctionnement du mécanisme de base de ce langage. Et une fois que vous avez vraiment compris comment fonctionne JavaScript lui-même, il est temps de commencer à utiliser ces puissantes capacités de métaprogrammation pour appliquer davantage le langage.

Cliquez pour suivre et découvrir les nouvelles technologies de Huawei Cloud dès que possible~

Linus a pris les choses en main pour empêcher les développeurs du noyau de remplacer les tabulations par des espaces. Son père est l'un des rares dirigeants capables d'écrire du code, son deuxième fils est directeur du département de technologie open source et son plus jeune fils est un noyau. contributeur à l'open source. Huawei : Il a fallu 1 an pour convertir 5 000 applications mobiles couramment utilisées Migration complète vers Hongmeng Java est le langage le plus sujet aux vulnérabilités tierces Wang Chenglu, le père de Hongmeng : l'open source Hongmeng est la seule innovation architecturale. dans le domaine des logiciels de base en Chine, Ma Huateng et Zhou Hongyi se serrent la main pour « éliminer les rancunes ». Ancien développeur de Microsoft : les performances de Windows 11 sont « ridiculement mauvaises » " Bien que ce que Laoxiangji est open source, ce ne soit pas le code, les raisons qui le sous-tendent. sont très réconfortants. Meta Llama 3 est officiellement publié. Google annonce une restructuration à grande échelle.
{{o.name}}
{{m.nom}}

Je suppose que tu aimes

Origine my.oschina.net/u/4526289/blog/11054218
conseillé
Classement