Was ist Metaprogrammierung in JavaScript? Auf Englisch bitte.

JavaScript bietet viele nützliche Funktionen, die die meisten Entwickler kennen. Gleichzeitig gibt es einige versteckte Juwelen, die wirklich herausfordernde Probleme lösen können, wenn Sie sich ihrer bewusst sind.

Metaprogrammierung in JavaScript ist ein solches Konzept, mit dem viele von uns möglicherweise nicht vertraut sind. In diesem Artikel erfahren Sie mehr über Metaprogrammierung und wie sie für uns nützlich ist.

Mit ES6 (ECMAScript 2015) unterstützen wir die Reflectund ProxyObjekte, mit denen wir problemlos Metaprogrammierung durchführen können. In diesem Artikel erfahren Sie, wie Sie sie anhand von Beispielen verwenden.

Was ist Metaprogrammierung?

Metaprogrammingist nichts weniger als die Magie in der Programmierung ! Wie wäre es mit einem Programm, das ein Programm liest, modifiziert, analysiert und sogar generiert? Klingt das nicht zauberhaft und kraftvoll?

So würde ich Metaprogramming als Entwickler beschreiben, der es ständig verwendet:

Metaprogrammingist eine Programmiertechnik, bei der Computerprogramme andere Programme als ihre Daten behandeln können. Dies bedeutet, dass ein Programm so gestaltet werden kann, dass es andere Programme liest, generiert, analysiert oder transformiert und sich sogar während der Ausführung selbst ändert.

Einfach ausgedrückt, beinhaltet Metaprogrammierung das Schreiben von Code, der dies kann

  • Code generieren
  • Bearbeiten Sie Sprachkonstrukte zur Laufzeit. Dieses Phänomen ist als Reflective Metaprogrammingoder bekannt Reflection.

Was ist Reflexion in der Metaprogrammierung?

Reflectionist ein Zweig der Metaprogrammierung. Reflexion hat drei Unterzweige:

  1. Selbstbeobachtung : Code kann sich selbst überprüfen. Es wird verwendet, um Informationen auf sehr niedriger Ebene über den Code zu ermitteln.
  2. Selbständerung : Wie der Name schon sagt, kann sich der Code selbst ändern.
  3. Fürbitte : Im Namen eines anderen handeln. Dies kann durch Umwickeln, Einfangen und Abfangen erreicht werden.

ES6 gibt uns das ReflectZiel (auch bekannt als Reflect API) zu erreichen Introspection. Das ProxyObjekt von ES6 hilft uns dabei Intercession. Wir werden nicht zu viel darüber reden,   Self-Modificationda wir uns so weit wie möglich davon fernhalten wollen.

Warte eine Sekunde! Um ganz klar zu sein, wurde Metaprogramming in ES6 nicht eingeführt. Vielmehr war es von Anfang an in der Sprache verfügbar. ES6 hat die Bedienung erheblich vereinfacht.

Ära der Metaprogrammierung vor ES6

Erinnerst du dich eval? Schauen wir uns an, wie es verwendet wurde:

const blog = { name: 'freeCodeCamp' } console.log('Before eval:', blog); const key = 'author'; const value = 'Tapas'; testEval = () => eval(`blog.${key} = '${value}'`); // Call the function testEval(); console.log('After eval magic:', blog); 

Wie Sie vielleicht bemerken, evalhalf das bei der zusätzlichen Codegenerierung. In diesem Fall wurde das Objekt blogzur Ausführungszeit mit einer zusätzlichen Eigenschaft geändert.

Before eval: {name: freeCodeCamp} After eval magic: {name: "freeCodeCamp", author: "Tapas"} 

Selbstbeobachtung

Vor der Aufnahme von Reflect objectin ES6 konnten wir noch eine Selbstbeobachtung durchführen. Hier ist ein Beispiel zum Lesen der Programmstruktur:

var users = { 'Tom': 32, 'Bill': 50, 'Sam': 65 }; Object.keys(users).forEach(name => { const age = users[name]; console.log(`User ${name} is ${age} years old!`); }); 

Hier lesen wir die usersObjektstruktur und protokollieren den Schlüsselwert in einem Satz.

User Tom is 32 years old! User Bill is 50 years old! User Sam is 65 years old! 

Selbstmodifikation

Nehmen wir ein Blog-Objekt, das eine Methode zum Ändern hat:

var blog = { name: 'freeCodeCamp', modifySelf: function(key, value) {blog[key] = value} } 

Das blogObjekt kann sich folgendermaßen ändern:

blog.modifySelf('author', 'Tapas'); 

Fürbitte

Intercessiongeht es darum, im Namen von etwas anderem zu handeln, indem die Semantik der Sprache geändert wird. Die   Object.defineProperty()Methode kann die Semantik eines Objekts ändern:

var sun = {}; Object.defineProperty(sun, 'rises', { value: true, configurable: false, writable: false, enumerable: false }); console.log('sun rises', sun.rises); sun.rises = false; console.log('sun rises', sun.rises); 

Ausgabe,

sun rises true sun rises true 

Wie Sie sehen, wurde das sunObjekt als normales Objekt erstellt und anschließend wurde die Semantik so geändert, dass sie nicht beschreibbar ist.

Lassen Sie uns nun die Reflectund ProxyObjekte mit ihren jeweiligen Verwendungen verstehen .

Die Reflect-API

In ES6 ist Reflect eine neue Global ObjectVersion (wie Mathematik), die eine Reihe von Dienstprogrammfunktionen bereitstellt, von denen sich viele mit den global definierten ES5-Methoden zu überschneiden scheinen Object.

Alle diese Funktionen sind Introspection-Funktionen, mit denen Sie zur Laufzeit einige interne Details zum Programm abfragen können.

Hier ist die Liste der verfügbaren Methoden aus dem ReflectObjekt. Bitte besuchen Sie diese Seite, um weitere Details zu jeder dieser Methoden zu sehen.

// Reflect object methods Reflect.apply() Reflect.construct() Reflect.get() Reflect.has() Reflect.ownKeys() Reflect.set() Reflect.setPrototypeOf() Reflect.defineProperty() Reflect.deleteProperty() Reflect.getOwnPropertyDescriptor() Reflect.getPrototypeOf() Reflect.isExtensible() 

But wait, here's a question: Why do we need a new API object when these could just exist already or could be added to Object or Function?

Confused? Let's try to figure this out.

All in one namespace

JavaScript already had support for object reflection. But these APIs were not organized under one namespace. Since ES6 they're now under Reflect.

Unlike most global objects, Reflect is not a constructor. You cannot use it with a new operator or invoke the Reflect object as a function. All properties and methods of Reflect are static like the math object.

Simple to use

The introspection methods of Object throw an exception when they fail to complete the operation. This is an added burden to the consumer (programmer) to handle that exception in the code.

You may prefer to handle it as a boolean(true | false) instead of using exception handling. The Reflect object helps you do that.

Here's an example with Object.defineProperty:

 try { Object.defineProperty(obj, name, desc); // property defined successfully } catch (e) { // possible failure and need to do something about it }

And with the Reflect API:

if (Reflect.defineProperty(obj, name, desc)) { // success } else { // failure (and far better) } 

The impression of the First-Class operation

We can find the existence of a property for an object as (prop in obj). If we need to use it multiple times in our code, we must explicitly wrap this operation in a function and pass the operation around as a first-class value.

In ES6, we already had those as part of the Reflect API as the first-class function. For example, Reflect.has(obj, prop) is the functional equivalent of (prop in obj).

Let's look at another example: Delete an object property.

const obj = { bar: true, baz: false}; // delete object[key] function deleteProperty(object, key) { delete object[key]; } deleteProperty(obj, 'bar'); 

With the Reflect API:

// With Reflect API Reflect.deleteProperty(obj, 'bar'); 

A more reliable way of using the apply() method

In ES5, we can use the apply() method to call a function with a given this value and passing an array as an argument.

Function.prototype.apply.call(func, obj, arr); // or func.apply(obj, arr); 

This is less reliable because func could be an object that would have defined its own apply method.

In ES6 we have a more reliable and elegant way of solving this:

Reflect.apply(func, obj, arr); 

In this case, we will get a TypeError if func is not callable. Also, Reflect.apply() is less verbose and easier to understand.

Helping other kinds of reflection

Wewill see what this means in a bit when we learn about the Proxy object. The Reflect API methods can be used with Proxy in many use cases.

The Proxy Object

ES6's Proxy object helps in intercession.

The proxy object defines custom behaviors for fundamental operations (for example, property lookup, assignment, enumeration, function invocation, and so on).

Here are a few useful terms you need to remember and use:

  • The target: An object which the proxy virtualizes.
  • The handler: A placeholder object which contains traps.
  • The trap: Methods that provide property access to the target object.

It is perfectly fine if you don't quite understand yet from the description above. We will get a grasp of it through code and examples in a minute.

The syntax to create a Proxy object is as follows:

let proxy = new Proxy(target, handler); 

There are many proxy traps (handler functions) available to access and customize a target object. Here is the list of them. You can read a more detailed description of traps here.

handler.apply() handler.construct() handler.get() handler.has() handler.ownKeys() handler.set() handler.setPrototypeOf() handler.getPrototypeOf() handler.defineProperty() handler.deleteProperty() handler.getOwnPropertyDescriptor() handler.preventExtensions() handler.isExtensible() 

Note that each of the traps has a mapping with the Reflect object's methods. This means that you can use Reflect and Proxy together in many use cases.

How to get unavailable object property values

Let's look at an example of an employee object and try to print some of its properties:

const employee = { firstName: 'Tapas', lastName: 'Adhikary' }; console.log(employee.firstName); console.log(employee.lastName); console.log(employee.org); console.log(employee.fullName); 

The expected output is the following:

Tapas Adhikary undefined undefined 

Now let's use the Proxy object to add some custom behavior to the employee object.

Step 1: Create a Handler that uses a get trap

We will use a trap called get which lets us get a property value. Here is our handler:

let handler = { get: function(target, fieldName) { if(fieldName === 'fullName' ) { return `${target.firstName} ${target.lastName}`; } return fieldName in target ? target[fieldName] : `No such property as, '${fieldName}'!` } }; 

The above handler helps to create the value for the fullName property. It also adds a better error message when an object property is missing.

Step 2: Create a Proxy Object

As we have the target employee object and the handler, we will be able to create a Proxy object like this:

let proxy = new Proxy(employee, handler); 

Step 3: Access the properties on the Proxy object

Now we can access the employee object properties using the proxy object, like this:

console.log(proxy.firstName); console.log(proxy.lastName); console.log(proxy.org); console.log(proxy.fullName); 

The output will be:

Tapas Adhikary No such property as, 'org'! Tapas Adhikary 

Notice how we have magically changed things for the employee object!

Proxy for Validation of Values

Let's create a proxy object to validate an integer value.

Step 1: Create a handler that uses a set trap

The handler looks like this:

const validator = { set: function(obj, prop, value) { if (prop === 'age') { if(!Number.isInteger(value)) { throw new TypeError('Age is always an Integer, Please Correct it!'); } if(value < 0) { throw new TypeError('This is insane, a negative age?'); } } } }; 

Step 2: Create a Proxy Object

Create a proxy object like this:

let proxy = new Proxy(employee, validator); 

Step 3: Assign a non-integer value to a property, say, age

Try doing this:

proxy.age = 'I am testing a blunder'; // string value 

The output will be like this:

TypeError: Age is always an Integer, Please Correct it! at Object.set (E:\Projects\KOSS\metaprogramming\js-mtprog\proxy\userSetProxy.js:28:23) at Object. (E:\Projects\KOSS\metaprogramming\js-mtprog\proxy\userSetProxy.js:40:7) at Module._compile (module.js:652:30) at Object.Module._extensions..js (module.js:663:10) at Module.load (module.js:565:32) at tryModuleLoad (module.js:505:12) at Function.Module._load (module.js:497:3) at Function.Module.runMain (module.js:693:10) at startup (bootstrap_node.js:188:16) at bootstrap_node.js:609:3 

Similarly, try doing this:

p.age = -1; // will result in error 

How to use Proxy and Reflect together

Here is an example of a handler where we use methods from the Reflect API:

const employee = { firstName: 'Tapas', lastName: 'Adhikary' }; let logHandler = { get: function(target, fieldName) { console.log("Log: ", target[fieldName]); // Use the get method of the Reflect object return Reflect.get(target, fieldName); } }; let func = () => { let p = new Proxy(employee, logHandler); p.firstName; p.lastName; }; func();

A few more Proxy use cases

There are several other use-cases where this concept can be used.

  • To protect the ID field of an object from deletion (trap: deleteProperty)
  • To trace Property Accesses (trap: get, set)
  • For Data Binding (trap: set)
  • With revocable references
  • To manipulate the in operator behavior

... and many more.

Metaprogramming Pitfalls

While the concept of Metaprogramming gives us lots of power, the magic of it can go the wrong way sometimes.

Be careful of:

  • Too much magic! Make sure you understand it before you apply it.
  • Possible performance hits when you're making the impossible possible
  • Could be seen as counter-debugging.

In Summary

To summarize,

  • Reflect and Proxy are great inclusions in JavaScript to help with Metaprogramming.
  • Lots of complex situations can be handled with their help.
  • Be aware of the downsides as well.
  • ES6 Symbols also can be used with your existing classes and objects to change their behavior.

I hope you found this article insightful. All the source code used in this article can be found in my GitHub repository.

Please share the article so others can read it as well. You can @ me on Twitter (@tapasadhikary) with comments, or feel free to follow me.