JavaScript tiene muchas características útiles que la mayoría de los desarrolladores conocen. Al mismo tiempo, hay algunas gemas ocultas que pueden resolver problemas realmente desafiantes si los conoces.
La metaprogramación en JavaScript es uno de esos conceptos con el que muchos de nosotros no estamos familiarizados. En este artículo, aprenderemos sobre la metaprogramación y cómo nos resulta útil.
Con ES6 (ECMAScript 2015), tenemos soporte para los objetos Reflect
y Proxy
que nos permiten hacer Metaprogramación con facilidad. En este artículo, aprenderemos a usarlos con ejemplos.
¿Qué es la metaprogramación?
Metaprogramming
es nada menos que la magia de la programación ! ¿Qué tal escribir un programa que lee, modifica, analiza e incluso genera un programa? ¿No suena mágico y poderoso?

Así es como describiría la metaprogramación como un desarrollador que la usa todo el tiempo:
Metaprogramming
es una técnica de programación en la que los programas de computadora tienen la capacidad de tratar otros programas como sus datos. Esto significa que un programa puede diseñarse para leer, generar, analizar o transformar otros programas, e incluso modificarse a sí mismo mientras se ejecuta.
En pocas palabras, la metaprogramación implica escribir código que
- Generar codigo
- Manipule construcciones de lenguaje en tiempo de ejecución. Este fenómeno se conoce como
Reflective Metaprogramming
oReflection
.
¿Qué es la reflexión en la metaprogramación?
Reflection
es una rama de la metaprogramación. La reflexión tiene tres ramas secundarias:
- Introspección : el código puede inspeccionarse a sí mismo. Se utiliza para descubrir información de muy bajo nivel sobre el código.
- Auto-modificación : como sugiere el nombre, el código puede modificarse a sí mismo.
- Intercesión : Actuar en nombre de otra persona. Esto se puede lograr envolviendo, atrapando, interceptando.
ES6 nos da el Reflect
objeto (también conocido como Reflect API) para lograr Introspection
. El Proxy
objeto de ES6 nos ayuda Intercession
. No hablaremos demasiado de ello, Self-Modification
ya que queremos alejarnos de él tanto como sea posible.
¡Espera un segundo! Para que quede claro, la metaprogramación no se introdujo en ES6. Más bien, ha estado disponible en el idioma desde sus inicios. ES6 simplemente lo hizo mucho más fácil de usar.
Era anterior a ES6 de la metaprogramación
¿Recuerdas eval
? Echemos un vistazo a cómo se usó:
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);
Como puede notar, eval
ayudó con la generación de código adicional. En este caso, el objeto blog
se ha modificado con una propiedad adicional en el momento de la ejecución.
Before eval: {name: freeCodeCamp} After eval magic: {name: "freeCodeCamp", author: "Tapas"}
Introspección
Antes de la inclusión del Reflect object
en ES6, todavía podíamos hacer introspección. Aquí hay un ejemplo de lectura de la estructura del programa:
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!`); });
Aquí estamos leyendo la users
estructura del objeto y registrando el valor-clave en una oración.
User Tom is 32 years old! User Bill is 50 years old! User Sam is 65 years old!
Auto modificación
Tomemos un objeto de blog que tiene un método para modificarse a sí mismo:
var blog = { name: 'freeCodeCamp', modifySelf: function(key, value) {blog[key] = value} }
El blog
objeto puede modificarse a sí mismo haciendo esto:
blog.modifySelf('author', 'Tapas');
Intercesión
Intercession
se trata de actuar en nombre de otra cosa cambiando la semántica del lenguaje. El Object.defineProperty()
método puede cambiar la semántica de un objeto:
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);
Salida,
sun rises true sun rises true
Como puede ver, el sun
objeto se creó como un objeto normal y luego se cambió la semántica para que no se pueda escribir.
Ahora pasemos a comprender los objetos Reflect
y Proxy
con sus respectivos usos.
La API Reflect
En ES6, Reflect es una nueva Global Object
(como matemática) que proporciona una serie de funciones de utilidad, muchas de las cuales parecen superponerse con los métodos de ES5 definidos en el global Object
.
Todas estas funciones son funciones de introspección donde puede consultar algunos detalles internos sobre el programa en el tiempo de ejecución.
Aquí está la lista de métodos disponibles del Reflect
objeto. Visite esta página para ver más detalles de cada uno de estos métodos.
// 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
andProxy
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.