Introducción a los proxies de JavaScript en ES6

I

Introducción

En este artículo, vamos a hablar sobre los proxies de JavaScript que se introdujeron con la versión de JavaScript. ECMAScript 6 (ES6). Usaremos parte de la sintaxis de 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?

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 validación y 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 capa intermedia, de cambiar cómo 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 empezar, creemos un objeto persona con firstName, lastNamey age propiedades:

const person = {
    firstName: 'John',
    lastName: 'Doe',
    age: 21
};

Ahora creemos un proxy simple pasándolo al Proxy constructor. Acepta parámetros llamados target y el handler. Ambos se desarrollarán en breve.

Primero creemos un objeto controlador:

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

Así es como puede crear un proxy simple:

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:

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 JavaScript. Entonces, usando ese ejemplo, familiaricémonos con algunas terminologías.

Destino de proxy

El primer parámetro, target, es el objeto al que ha adjuntado el proxy. El proxy utilizará este objeto para almacenar datos, lo que significa que si cambia el valor del objeto de destino, el valor del objeto del 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 creando 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:

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

Controlador de proxy

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

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

Por ejemplo, puede crear un proxy que actualice una caché o una base de datos cada vez que se actualice una propiedad del objeto de destino.

Trampas de proxy

La trampa get ()

los get() atrapar incendios 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 una convención, los desarrolladores usan el guión bajo (_) delante del nombre de la propiedad, por ejemplo, _securityNumber, para identificarlo como propiedad privada.

Sin embargo, esto en realidad no aplica 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 person objeto con un número de seguro social en una propiedad llamada _ssn:

const person = {
    firstName: 'John',
    lastName: 'Doe',
    age: 21,
    _ssn: '123-45-6789'
};

Ahora editemos el get() trap para lanzar una excepción si alguien intenta acceder a una propiedad que comienza con un guión bajo:

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:

Error: _ssn is a private property

La trampa del set ()

Ahora, echemos un vistazo a set() trap, que controla el comportamiento al establecer valores en la propiedad de un objeto de destino. Para darle un ejemplo claro, supongamos que cuando define un person objetar el valor de la age 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 cualquier momento. Así que normalmente es muy difícil hacer cumplir la age propiedad para contener enteros. Sin embargo, con los proxies, podemos controlar la forma en que establecemos los valores de las propiedades:

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, el set() trap acepta tres parámetros, que son:

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

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

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

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 al deleteProperty() trampa que se activará cuando intente eliminar una propiedad de un objeto:

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, el deleteProperty() trampa también acepta el target y property parámetros.

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

You have deleted age

Usar proxies con funciones

La trampa de aplicar ()

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

const person = {
    firstName: 'Sherlock',
    lastName: 'Holmes'
};

Luego, un método para obtener el nombre completo:

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

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

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, el apply() trap será llamado cuando se llame a la función. Acepta tres parámetros: target, thisArg (Cuál es el this argumento para la llamada), y el args, que es la lista de argumentos pasados ​​a la función.

Hemos utilizado el apply() trap para ejecutar la función de destino con los argumentos dados utilizando la sintaxis de propagación de ES6 y convertir el resultado a mayúsculas. Entonces debería ver el nombre completo en mayúsculas:

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 person objeto con las propiedades firstName y lastName. Con esto, el nombre completo puede ser una combinación de esas propiedades, como en nuestro último ejemplo. Por tanto, el nombre completo es una propiedad calculada.

Primero, creemos de nuevo un person objeto con un nombre y un apellido:

const person = {
    firstName: 'John',
    lastName: 'Doe'
};

Entonces podemos crear un controlador con el get() trap para devolver el nombre completo calculado, que se logra creando un proxy del person:

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:

console.log(proxyPerson.fullName);
John Doe

Utilizando solo el proxy, hemos creado un método “getter” en el person objeto sin tener que cambiar realmente el objeto original.

Ahora, veamos otro ejemplo que es más dinámico de 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 variedad de personas, donde cada objeto tiene un id de la persona, nombre de la persona y la edad de la persona. Necesitamos consultar a una persona por el id, nameo age. Así que simplemente podemos crear algunos métodos, getById, getByNamey getByAge. Pero esta vez vamos a llevar las cosas un poco más lejos.

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

Pero por el momento creemos una variedad de personas.

const people = [
    {
        id: 1,
        name: 'John Doe',
        age: 21
    },
    {
        id: 2,
        name: 'Ann Clair',
        age: 24
    },
    {
        id: 3,
        name: 'Sherlock Holmes',
        age: 35
    }
];

Ahora creemos un get trap para generar la función dinámica según el nombre de la función.

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 elemento. Entonces, por ejemplo, si el nombre de la propiedad es getById, terminamos con id como propiedad de 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.

Puede probar esto ejecutando lo siguiente:

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:

{ 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 propiedades isbn, titley author. Luego, usando un código similar al anterior, vea cómo puede usar getByIsbn, getByTitley getByAuthor para recuperar elementos de la lista.

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

Conclusión

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

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad