Introducción a los proxies de JavaScript en ES6

Los proxies tienen la capacidad de cambiar el comportamiento fundamental de objetos y funciones. En este artículo, hablaremos sobre los proxies de JavaScript, introducidos en ES6 y mostraremos su funcionalidad.

Introducción

En este artículo, hablaremos sobre los proxies de JavaScript que se introdujeron con la versión de JavaScript ECMAScript 6 (ES6). Usaremos parte de la sintaxis ES6 existente, incluido el operador de propagación en este artículo. Por lo tanto, será útil si tiene algunos conocimientos básicos sobre ES6.

¿Qué es un Proxy? {#lo que es un proxy}

Los proxies de JavaScript tienen la capacidad de cambiar el comportamiento fundamental de objetos y funciones. Podemos extender el lenguaje para que se adapte mejor a nuestros requisitos o simplemente usarlo para cosas como la validación y el control de acceso en una propiedad.

Hasta que se introdujeron los proxies, no teníamos acceso de nivel nativo para cambiar el comportamiento fundamental de un objeto, ni una función. Pero con ellos, tenemos la capacidad de actuar como una capa intermedia, cambiar la forma en que se debe acceder al objeto, generar información como cuántas veces se ha llamado a una función, etc.

Ejemplo de proxy de propiedad

Comencemos con un ejemplo simple para ver los proxies en acción. Para comenzar, vamos a crear un objeto de persona con las propiedades firstName, lastName y age:

1
2
3
4
5
const person = {
    firstName: 'John',
    lastName: 'Doe',
    age: 21
};

Ahora vamos a crear un proxy simple pasándolo al constructor Proxy. Acepta parámetros llamados target y handler. Ambos serán elaborados en breve.

Primero vamos a crear un objeto controlador:

1
2
3
4
5
6
const handler = {
    get(target, property) {
        console.log(`you have read the property ${property}`);
        return target[property];
    }
};

Así es como puedes crear un proxy simple:

1
2
3
4
5
const proxyPerson = new Proxy(person, handler);

console.log(proxyPerson.firstName);
console.log(proxyPerson.lastName);
console.log(proxyPerson.age);

Ejecutar este código debería producir:

1
2
3
4
5
6
you have read the property firstName
John
you have read the property lastName
Doe
you have read the property age
21

Cada vez que acceda a una propiedad de ese objeto proxy, recibirá un mensaje de consola con el nombre de la propiedad. Este es un ejemplo muy simple de un proxy de JavaScript. Entonces, usando ese ejemplo, familiaricémonos con algunas terminologías.

Proxy objetivo

El primer parámetro, objetivo, es el objeto al que ha adjuntado el proxy. Este objeto será utilizado por el proxy para almacenar datos, lo que significa que si cambia el valor del objeto de destino, el valor del objeto proxy también cambiará.

Si desea evitar esto, puede pasar el objetivo directamente al proxy como un objeto anónimo, o puede usar algún método de encapsulación para proteger el objeto original mediante la creación de una expresión de función invocada inmediatamente (IIFE), o un singleton

Simplemente no exponga su objeto al exterior donde se usará el proxy y todo debería estar bien.

Un cambio en el objeto de destino original todavía se refleja en el proxy:

1
2
3
console.log(proxyPerson.age);
person.age = 20;
console.log(proxyPerson.age);
1
2
3
4
you have read the property age
21
you have read the property age
20

Manejador de proxy {#manejador de proxy}

El segundo parámetro del constructor Proxy es el handler, que debe ser un objeto que contenga métodos que describan la forma en que desea controlar el comportamiento del objetivo. Los métodos dentro de este controlador, por ejemplo, el método get(), se llaman trampas.

Al definir un controlador, como el que hemos definido en nuestro ejemplo anterior, podemos escribir una lógica personalizada para un objeto que, de lo contrario, no la implementaría.

Por ejemplo, podría crear un proxy que actualice un caché o una base de datos cada vez que se actualice una propiedad en el objeto de destino.

Trampas de proxy {#trampas de proxy}

La trampa get()

La trampa get() se activa cuando alguien intenta acceder a una propiedad específica. En el ejemplo anterior, usamos esto para imprimir una oración cuando se accedió a la propiedad.

Como ya sabrá, JavaScript no admite propiedades privadas. Entonces, a veces, como convención, los desarrolladores usan el guión bajo (_) delante del nombre de la propiedad, por ejemplo, _securityNumber, para identificarlo como una propiedad privada.

Sin embargo, esto en realidad no impone nada en el nivel de código. Los desarrolladores simplemente saben que no deben acceder directamente a las propiedades que comienzan con _. Con proxies, podemos cambiar eso.

Actualicemos nuestro objeto persona con un número de seguro social en una propiedad llamada _ssn:

1
2
3
4
5
6
const person = {
    firstName: 'John',
    lastName: 'Doe',
    age: 21,
    _ssn: '123-45-6789'
};

Ahora vamos a editar la trampa get() para lanzar una excepción si alguien intenta acceder a una propiedad que comienza con un guión bajo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const handler = {
    get(target, property) {
        if (property[0] === '_') {
            throw new Error(`${property} is a private property`);
        }

        return target[property];
    }
}

const proxyPerson = new Proxy(person, handler);

console.log(proxyPerson._ssn);

Si ejecuta este código, debería ver el siguiente mensaje de error en su consola:

1
Error: _ssn is a private property

La trampa set()

Ahora, echemos un vistazo a la trampa set (), que controla el comportamiento al establecer valores en la propiedad de un objeto de destino. Para darte un ejemplo claro, supongamos que cuando defines un objeto persona el valor de edad debe estar en el rango de 0 a 150.

Como ya sabrá, JavaScript es un lenguaje de escritura dinámico, lo que significa que una variable puede contener cualquier tipo de valor (cadena, número, bool, etc.) en un momento dado. Por lo tanto, normalmente es muy difícil hacer cumplir la propiedad age para que solo contenga números enteros. Sin embargo, con proxies, podemos controlar la forma en que establecemos los valores de las propiedades:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const handler = {
    set(target, property, value) {
        if (property === 'age') {
            if (!(typeof value === 'number')) {
                throw new Error('Age should be a number');
            }

            if (value < 0 || value > 150) {
                throw new Error("Age value should be in between 0 and 150");
            }
        }

        target[property] = value;
    }
};

const proxyPerson = new Proxy(person, handler);
proxyPerson.age = 170;

Como puede ver en este código, la trampa set () acepta tres parámetros, que son:

  • objetivo: el objeto de destino al que se adjuntó el proxy
  • propiedad: El nombre de la propiedad que se está configurando
  • valor: El valor que se asigna a la propiedad

En esta trampa, hemos verificado si el nombre de la propiedad es edad y, de ser así, si también es un número y el valor está entre 0 y 150, arrojando un error si no lo es.

Cuando ejecute este código, debería ver el siguiente mensaje de error en la consola:

1
Error: Age value should be in between 0 and 150

Además, puede intentar asignar un valor de cadena y ver si arroja un error.

La trampa deleteProperty()

Ahora pasemos a la trampa deleteProperty() que se activará cuando intente eliminar una propiedad de un objeto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const handler = {
    deleteProperty(target, property) {
        console.log('You have deleted', property);
        delete target[property];
    }
};

const proxyPerson = new Proxy(person, handler);

delete proxyPerson.age;

Como puede ver, la trampa deleteProperty() también acepta los parámetros objetivo y propiedad.

Si ejecuta este código, debería ver el siguiente resultado:

1
You have deleted age

Uso de servidores proxy con funciones {#uso de servidores proxy con funciones}

La trampa apply()

La trampa apply() se utiliza para identificar cuándo se produce una llamada de función en el objeto proxy. En primer lugar, vamos a crear una persona con un nombre y un apellido:

1
2
3
4
const person = {
    firstName: 'Sherlock',
    lastName: 'Holmes'
};

Luego un método para obtener el nombre completo:

1
2
3
const getFullName = (person) => {
    return person.firstName + ' ' + person.lastName;
};

Ahora, vamos a crear un método proxy que convertirá la salida de la función a letras mayúsculas proporcionando una trampa apply() dentro de nuestro controlador:

1
2
3
4
5
6
7
const getFullNameProxy = new Proxy(getFullName, {
    apply(target, thisArg, args) {
        return target(...args).toUpperCase();
    }
});

console.log(getFullNameProxy(person));

Como puede ver en este ejemplo de código, se llamará a la trampa apply() cuando se llame a la función. Acepta tres parámetros: target, thisArg (que es el argumento this para la llamada) y args, que es la lista de argumentos pasados ​​a la función.

Hemos usado la trampa apply() para ejecutar la función de destino con los argumentos dados usando la sintaxis de propagación de ES6 y convertimos el resultado a mayúsculas. Entonces deberías ver el nombre completo en mayúsculas:

1
SHERLOCK HOLMES

Propiedades calculadas con proxies

Las propiedades calculadas son las propiedades que se calculan realizando operaciones en otras propiedades existentes. Por ejemplo, digamos que tenemos un objeto persona con las propiedades nombre y apellido. Con esto, el nombre completo puede ser una combinación de esas propiedades, como en nuestro último ejemplo. Por lo tanto, el nombre completo es una propiedad calculada.

Primero, volvamos a crear un objeto persona con un nombre y un apellido:

1
2
3
4
const person = {
    firstName: 'John',
    lastName: 'Doe'
};

Luego podemos crear un controlador con la trampa get() para devolver el nombre completo calculado, lo que se logra creando un proxy de la persona:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const handler = {
    get(target, property) {
        if (property === 'fullName') {
            return target.firstName + ' ' + target.lastName;
        }

        return target[property];
    }
};

const proxyPerson = new Proxy(person, handler);

Ahora intentemos acceder al nombre completo de la persona proxy:

1
console.log(proxyPerson.fullName);
1
John Doe

Usando solo el proxy, hemos creado un método "captador" en el objeto persona sin tener que cambiar el objeto original en sí.

Ahora, veamos otro ejemplo que es más dinámico que lo que hemos encontrado hasta ahora. Esta vez, en lugar de devolver solo una propiedad, devolveremos una función que se crea dinámicamente en función del nombre de función dado.

Considere una matriz de personas, donde cada objeto tiene un “id” de la persona, el nombre de la persona y la edad de la persona. Necesitamos consultar a una persona por ‘id’, ’nombre’ o ’edad’. Así que simplemente podemos crear algunos métodos, getById, getByName y getByAge. Pero esta vez vamos a llevar las cosas un poco más allá.

Queremos crear un controlador que pueda hacer esto para una matriz que pueda tener cualquier propiedad. Por ejemplo, si tenemos una matriz de libros y cada libro tiene una propiedad isbn, también deberíamos poder consultar esta matriz usando getByIsbn y el método debería generarse dinámicamente en el tiempo de ejecución.

Pero por el momento vamos a crear una matriz de personas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const people = [
    {
        id: 1,
        name: 'John Doe',
        age: 21
    },
    {
        id: 2,
        name: 'Ann Clair',
        age: 24
    },
    {
        id: 3,
        name: 'Sherlock Holmes',
        age: 35
    }
];

Ahora vamos a crear una trampa get para generar la función dinámica de acuerdo con el nombre de la función.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const proxyPeople = new Proxy(people, {
    get(target, property) {
        if (property.startsWith('getBy')) {
            let prop = property.replace('getBy', '')
                               .toLowerCase();

            return function(value) {
                for (let i of target) {
                    if (i[prop] === value) {
                        return i;
                    }
                }
            }
        }

        return target[property];
    }
});

En este código, primero verificamos si el nombre de la propiedad comienza con "getBy", luego eliminamos "getBy" del nombre de la propiedad, por lo que terminamos con el nombre real de la propiedad que queremos usar para consultar el artículo. Entonces, por ejemplo, si el nombre de la propiedad es getById, terminamos con id como la propiedad por la cual realizar la consulta.

Ahora tenemos el nombre de propiedad con el que queremos consultar, por lo que podemos devolver una función que acepta un valor e iterar a través de la matriz para encontrar un objeto con ese valor y en la propiedad dada.

Puedes probar esto ejecutando lo siguiente:

1
2
3
console.log(proxyPeople.getById(1));
console.log(proxyPeople.getByName('Ann Clair'));
console.log(proxyPeople.getByAge(35));

El objeto de persona relevante para cada llamada debe mostrarse en la consola:

1
2
3
{ id: 1, name: 'John Doe', age: 21 }
{ id: 2, name: 'Ann Clair', age: 24 }
{ id: 3, name: 'Sherlock Holmes', age: 35 }

En la primera línea usamos proxyPeople.getById(1), que luego devolvió al usuario con un id de 1. En la segunda línea usamos proxyPeople.getByName('Ann Clair'), que devolvió a la persona con el nombre "Ann Clair", y así sucesivamente.

Como ejercicio para el lector, intente crear su propia matriz de libros con las propiedades isbn, title y author. Luego, usando un código similar al anterior, vea cómo puede usar getByIsbn, getByTitle y getByAuthor para recuperar elementos de la lista.

Para simplificar, en esta implementación hemos asumido que solo hay un objeto con un valor determinado para cada propiedad. Pero este podría no ser el caso en algunas situaciones, en las que luego puede editar ese método para devolver una matriz de objetos que coincidan con la consulta dada.

Conclusión

El código fuente de este artículo está disponible en GitHub como de costumbre. Use esto para comparar su código si se quedó atascado en el tutorial.