Articles

Una breve introducción a la Metaprogramación en JavaScript

Reflexión

Reflexión, «generación de código», es un proceso para cambiar la mecánica subyacente del lenguaje. La reflexión puede ocurrir en tiempo de compilación o en tiempo de ejecución, pero seguiremos con la reflexión en tiempo de ejecución ya que estamos hablando de JavaScript, por lo que la reflexión en tiempo de compilación no será posible. Sin embargo, los conceptos discutidos aquí también podrían ser aplicables a un lenguaje compilado.

Como hemos entendido que la reflexión se trata de cambiar la mecánica subyacente del lenguaje, se ha dividido en tres categorías principales, a saber: introspección, intercesión y modificación.

Introspección

La introspección es el proceso de análisis del programa. Si puedes saber lo que hace el programa, puedes modificarlo según tus gustos. Aunque algunos lenguajes de programación no admiten funciones de generación o modificación de código, lo más probable es que permitan la introspección.

Un ejemplo simple de introspección sería usar operadores typeof o instanceof en JavaScript. El typeof devuelve el tipo de datos actual de un valor (o una expresión que devuelve un valor) mientras que instanceof devuelve true o false si el valor LHS es una instancia de clase RHS. Vamos a verlos en acción.

(introducción/la introspección.js)

En el programa anterior, hemos utilizado typeof y instanceof operadores en el coerce función para detectar el tipo de datos de entrada value. Esta es la demostración básica de la introspección. Sin embargo, un lenguaje diseñado específicamente para la metaprogramación podría proporcionar algunas herramientas de introspección poderosas.

Puede utilizar el operador in para comprobar si existe una propiedad en el objeto. La función global isNaN comprueba si el objeto es NaN. Hay algunos métodos estáticos construidos alrededor del tipo Object para inspeccionar los valores del tipo Object, como Object.isFrozen(value) para verificar si value está congelado o Object.keys(value) para obtener los nombres de propiedad del objeto value.

Hasta ES5, teníamos estos operadores y estos métodos para trabajar. En ES2015 (ES6), JavaScript introdujo Reflect objeto que proporciona algunos métodos estáticos (al igual que Object) pero diseñado específicamente para la introspección. Dado que tenemos una lección separada sobre Reflect, estos métodos se discuten allí.

Intercesión

Intercesión es el proceso de intervenir en los procesos JavaScript y modificar el comportamiento estándar del proceso. JavaScript proporciona excelentes herramientas para intercesión, una de las cuales es Proxy.

La clase Proxy se introdujo en ES2015 (ES6) para interceptar (intervenir) operaciones básicas de JavaScript alrededor de objetos como hemos visto anteriormente, pero de una manera mucho más agradable. Tenemos una lección separada sobre Proxy(próximamente), pero en pocas palabras, Proxy envuelve una lógica interceptable alrededor de un objeto.

var targetWithProxy = new Proxy(target, handler);

Aquí, el target es el objeto y el handler es el interceptor. El handler también es un objeto JavaScript simple pero con algunos campos significativos. For example, handler.get would be a function that returns a custom value when target.prop (here, prop is any property) is accessed.

(introduction/proxy.js)

Proxy is a great way to provide abstractions over your not-so-public data. Por ejemplo, en el programa anterior, hemos proporcionado abstracciones sobre el objeto target y personalizado cómo debe presentarse al público.

También fue posible alguna intercesión en ES5, como usar getter y setters en descriptores de propiedades, pero resultaría en la mutación del objeto targetProxy proporciona una forma mucho más limpia de lograr la intercesión sin tener que modificar el objeto original (target).

Modificación

La modificación se refiere a la modificación del comportamiento del programa a través de la mutación. En el caso de intercesión, solo interceptamos los procesos estándar de JavaScript agregando una lógica de interceptación entre el objetivo y el receptor sin dañar al objetivo. En este caso de modificación, estamos cambiando el comportamiento del objetivo en sí para que se adapte al receptor.

Anular una implementación de función sería un buen ejemplo de modificación. Por ejemplo, si una función está diseñada para comportarse de cierta manera, pero queremos otra cosa condicionalmente, podemos hacer eso diseñando una función de auto sobreescritura. Veamos un ejemplo.

(introducción/la función de modificación.js)

En el ejemplo anterior, hemos creado una función que se reemplaza a sí misma con una nueva implementación de función. Este sería el ejemplo más duro de modificación, pero tenemos otros casos de uso, quizás más significativos.

(introducción/readonly-objeto.js)

En el ejemplo anterior, hemos utilizado el método Object.defineProperty() para cambiar el descriptor de propiedad predeterminado de la propiedad name para que sea de solo lectura. También puede usar el método Object.freeze() para bloquear todo el objeto y evitar cualquier mutación.

Algunas intercesiones pueden ocurrir a través de modificaciones, como se puede hacer en el ejemplo anterior. Al establecer writable:false en el descriptor de propiedad del objeto, por lo tanto mutando el objeto (implementación interna), hemos iniciado la operación de asignación de valores.

Si no está familiarizado con el método valueOf, se utiliza para obligar a un objeto a un valor primitivo. Por lo tanto, si tengo un objeto y tiene valueOf método en sí mismo o en su cadena de prototipo, JavaScript llama a este método cuando intenta realizar una operación aritmética en él. De forma predeterminada, Object tiene el método valueOf que se devuelve a sí mismo (el objeto).

(introducción/valueof.js)

Como puede ver en el ejemplo anterior, emp1/10resultó en un NaN ya que un objeto no se puede dividir como números naturales. Pero, más tarde, hemos añadido valueOf método Employee clase que devuelve salary valor del objeto. Por lo tanto emp2/10 devuelve 200 desde emp2.salary es 200. Del mismo modo, emp3/10 devuelve 300 como hemos añadido valueOf método directamente en el emp3.

Así que en cada paso del ejemplo anterior, estamos interviniendo cómo se presenta un objeto a una operación estándar de JavaScript y cambiando su comportamiento a nuestros gustos. Esto no es más que la intercesión.

En ES2015 (ES6), JavaScript ha introducido un nuevo tipo de datos primitivo que es symbol. No es nada como lo hemos visto antes y no se puede representar en forma literal. Solo se puede construir llamando a la función Symbol.

var sym1 = Symbol();
var sym2 = Symbol();
var sym3 = Symbol('description'); // description for debugging aidsym1 === sym2 // false
sym1 === sym2 // falsetypeof sym1 // 'symbol'console.log( sym1 ); // 'Symbol()'
console.log( sym3 ); // 'Symbol(description)'

En pocas palabras, produce valores únicos que también se pueden usar como claves de objeto regulares utilizando la notación obj donde key sería un símbolo.

var key = Symbol();var obj = {
name: 'Ross',
: 200
};console.log( obj.name ); // 'Ross'
console.log( obj ); // 200
Object.keys(obj); // obj = 300;

Dado que son únicos, no hay forma de crear un símbolo duplicado por accidente. Cada símbolo nuevo es único (creado usando Symbol()), lo que significa que si desea usar un mismo símbolo, debe almacenarlo en una variable y pasar esa variable para referirse al mismo símbolo.

En el ejemplo valueOf, puede detectar el problema si no estamos siendo cuidadosos o conscientes. Dado que valueOf es una propiedad string (como en emp3) , cualquiera puede anularla accidentalmente o alguien que no conozca valueOf podría planear usarla para su propio uso pensando «¿Qué es lo que ¿en el nombre?».

Dado que los símbolos también se pueden usar como claves de objeto, JavaScript ha proporcionado algunos símbolos globales que se deben usar como claves de objeto para algunas operaciones estándar de JavaScript. Dado que estos símbolos son bien conocidos por un desarrollador, se les llama «símbolos conocidos». Estos símbolos conocidos se exponen al público como las propiedades estáticas de la función Symbol.

Uno de los símbolos bien conocidos es Symbol.toPrimitive que debe usarse como la clave del objeto para obtener su valor primitivo. Sí, está pensando bien, es un reemplazo del método valueOf y es el preferido.

(introducción/símbolo-toPrimitive.js)

The El método toPrimitive hace más que devolver un valor numérico del objeto. Por favor, lea las lecciones de Símbolos para saber más al respecto.

JavaScript proporciona muchos de estos símbolos conocidos para interceptar y modificar el comportamiento predeterminado de JavaScript alrededor de los objetos. Hablaremos de esto y de los símbolos en general en la lección de Símbolos.