Introducción a los proxies de JavaScript en ES6

    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.

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *