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.
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.
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 target
Proxy
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.
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.
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).
Como puede ver en el ejemplo anterior, emp1/10
resultó 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.
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.