Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:proxies

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Ambos lados, revisión anteriorRevisión previa
Próxima revisión
Revisión previa
informatica:programacion:cursos:programacion_avanzada_javascript:proxies [2024/10/28 13:09] – [Creación de proxies] tempwininformatica:programacion:cursos:programacion_avanzada_javascript:proxies [2024/10/30 16:03] (actual) – [Recursos] tempwin
Línea 64: Línea 64:
 Enseguida lo vamos a ver en la práctica y se entenderá mucho mejor. Pero antes analicemos las posibles intercepciones en los proxies. Enseguida lo vamos a ver en la práctica y se entenderá mucho mejor. Pero antes analicemos las posibles intercepciones en los proxies.
 ===== Intercepciones básicas ===== ===== Intercepciones básicas =====
 +
 +Vamos a ver qué podemos interceptar con un proxy. Las intercepciones básicas son llamadas a //getters// de propiedad, a //setters// y a métodos.
 +
 +==== Getters de propiedad ====
 +
 +Ya lo hemos visto en la lección anterior: basta con definir un método ''get'' en el //handler// del proxy. Este método recibe tres parámetros:
 +
 +  * ''target'': es el propio objeto interceptado por el proxy
 +  * ''key'': el nombre de la propiedad
 +  * ''receiver'': el valor de la propiedad en el objeto
 +
 +Así p. ej. podríamos crear un proxy que crease una propiedad "fantasma" ''json'' que devolviese una representación en json del objeto:
 +
 +<code javascript>
 +let obj = {v: 42};
 +
 +let proxy = new Proxy(obj, {
 +   get (target, key, receiver) {
 +       if (key === "json") {
 +           return JSON.stringify(target);
 +       }
 +       else {
 +           return target[key];
 +       }
 +   }
 +});
 +
 +let str = proxy.json;       // "{"v":42}"
 +</code>
 +
 +==== Setters de propiedad ====
 +
 +Para interceptar un //setter// de una propiedad el //handler// debe definir un método ''set''. Los parámetros de este método set son los mismos que el del método ''get'' usado para interceptar un //getter//, con la excepción de que ''receiver'' ahora es el nuevo valor que se quiere asignar a la propiedad (no el que tiene actualmente asignado el objeto en esa propiedad, como antes).
 +
 +Vamos a crear un proxy que obligue a que todas las propiedades de un objeto tengan un valor de tipo cadena:
 +
 +<code javascript>
 +let obj = {v: "una cadena"};
 +let proxy = new Proxy(obj, {
 +   set (target, key, receiver) {
 +       if (typeof(receiver) === "string") {
 +           target[key] = receiver;
 +       }
 +   }
 +});
 +
 +proxy.v = 42;
 +proxy.v;            // Sigue teniendo el valor original, "una cadena", pues no se puede asignar un número
 +proxy.v = "ahora sí";
 +proxy.v;            // "ahora sí"
 +</code>
 +
 +El uso de proxies es muy sencillo y nos da una potencia brutal, permitiéndonos realizar tareas que sin ellos serían mucho más difíciles.
 +
 +==== Llamadas a métodos ====
 +
 +Para llamar a métodos se usa también el método ''get'' del //handler//. De hecho JavaScript no distingue entre //getters// de propiedades y llamadas a métodos.
 +
 +Cuando tenemos un código tipo ''o.f()'', primero se llama al //getter// de la propiedad ''f'' del objeto ''o'' y si el valor devuelto es una función se invoca a dicha función (en caso contrario el motor de JavaScript da un error). Así, lo que debemos tener claro es que en el //handler// debemos devolver una función, porque sino, éste recibirá un error:
 +
 +<code javascript>
 +var obj = {
 +v: 42,
 +inc(value) {this.v += value; return this.v; }
 +}
 +
 +let proxy = new Proxy(obj, {
 +   get (target, key, receiver) {
 +      var prop = target[key];
 +      if (typeof(prop) === "function") {
 +          return function(...args) {
 +              let result = prop.apply(this, args);
 +              console.log(`${key}(${JSON.stringify(args)}) -> ${result != undefined ?  JSON.stringify(result) : undefined}`);
 +              return result;
 +          }
 +      }
 +      else return target[key];
 +   }
 +});
 +</code>
 +
 +Este código intercepta todas las llamadas a todos métodos y las imprime por la consola. Observa el uso de ''...args'' en la función devuelta por el proxy. Al tener un solo parámetro rest, eso lo que hace es que ''args'' sea un array con los valores pasados a la función.
 +
 +De este modo cuando hacemos ''proxy.inc(10)'' sucede lo siguiente:
 +
 +  * Se llama al método ''get'' del //handler//
 +  * Este método obtiene el valor de la propiedad indicada (''inc'')
 +  * Si la propiedad es una función (en este caso lo es), devuelve otra función que llama a la función del objeto original (usando ''apply'' y luego imprime la llamada por la consola (y finalmente devuelve el resultado de haber llamado a la función original). La idea es que llamar a ''proxy.inc(10)'' termina llamando realmente a la función interna (que tiene el parámetro //rest// ''args'') con el valor de ''10''. Por lo tanto dentro de esa función el valor de ''args'' es un array con un elemento: ''10''. Así podemos usar fácilmente ''apply'' para obtener el valor real y luego hacer lo que queramos.
 +  * Si la propiedad llamada no fuese una función (p. ej. hiciéramos ''proxy.v'') pues directamente se devuelve el valor de invocar a dicha propiedad en el objeto real.
  
 ===== DEMO: Interceptando un getter a propiedad ===== ===== DEMO: Interceptando un getter a propiedad =====
  
 +Lo importante de los //proxies// es entender que envuelven un objeto y luego nos permiten interceptar llamadas a las propiedades y métodos del objeto y terminar delegando esta llamada a la propiedad real del objeto o devolver cualquier otra cosa a quien hace la llamada.
 +
 +Partimos de un objeto sencillo:
 +
 +<code javascript>
 +var obj = {
 +    prop: 42
 +}
 +</code>
 +
 +Ahora podemos tener una función así:
 +
 +<code javascript>
 +function foo(p) {
 +    console.log(p.prop)
 +}
 +</code>
 +
 +Al usarla:
 +
 +<code javascript>
 +foo(obj); // 42
 +</code>
 +
 +Vamos a crear un proxy para envolver el objeto:
 +
 +<code javascript>
 +let proxy = new Proxy(obj, {
 +    get(target, key, receiver) {
 +        console.log(target, key, receiver);
 +    }
 +})
 +</code>
 +
 +Ahora a la función debemos pasarle el proxy, no el objeto original:
 +
 +<code javascript>
 +foo(proxy); // Object { prop: 42 } prop Object { prop: 42 }
 +</code>
 +
 +Modificamos el proxy:
 +
 +<code javascript>
 +let proxy = new Proxy(obj, {
 +    get(target, key, receiver) {
 +        if (key == "prop") {
 +            return target[key];
 +        } else {
 +            return NaN;
 +        }
 +        
 +    }
 +})
 +</code>
 +
 +<code javascript>
 +foo(proxy); // 42
 +</code>
 +
 +Si modificamos la función ''foo'' tal que así:
 +
 +<code javascript>
 +function foo(p) {
 +    console.log(p.prop)
 +    console.log(p.cualquierCosa);
 +}
 +</code>
 +
 +Y ejecutamos:
 +
 +<code javascript>
 +foo(proxy); // 42
 +            // NaN
 +</code>
 +
 +Hemos visto cómo el proxy nos ha permitido envolver de manera sencilla un objeto y expandir su comportamiento sin necesidad de tocar el objeto original.
 ===== Ejercicio propuesto ===== ===== Ejercicio propuesto =====
  
 +Se propone el siguiente ejercicio: Usa proxies para ocultar automáticamente de un objeto todas aquellas propiedades que empiecen por un subrayado. Es decir, dado un objeto:
 +
 +<code javascript>
 +let obj = {
 +   _pr: 10,
 +   pu: 20
 +};
 +</code>
 +
 +Se quiere que la propiedad ''_pr'' no sea accesible haciendo ''obj._pr''. Intentar acceder a la propiedad para leerla debe devolver ''undefined'' y acceder para modificar su valor debe ser ignorado.
 +
 +Tienes la solución en el siguiente apartado y en el fichero ''privateHidder.js'' pero inténtalo antes por tu cuenta.
 +
 +==== DEMO: Solución al ejercicio de ocultar variables privadas ====
 +
 +Partamos de:
 +
 +<code javascript>
 +var obj = {
 +    _v: 10,
 +    set v(value) {
 +        // Solo para valores positivos
 +        if (value > 0) {
 +            this._v = value;
 +        }
 +    }
 +}
 +</code>
 +
 +Probamos:
 +
 +<code javascript>
 +obj.v = 1200;
 +obj._v = -10;
 +
 +console.log(obj._v); // -10
 +</code>
 +
 +Ahora, vamos a usar un proxy para ocultar una variable privada y que no podamos ejecutar lo anterior.
 +
 +<code javascript>
 +const hidePrivates = function(obj) {
 +    const handler = {
 +        get (target, key, receiver) {
 +            if (key.substr(0, 1) === "_") {
 +                return undefined;
 +            } else {
 +                return target[key];
 +            }
 +        },
 +        set (target, key, receiver) {
 +            if (key.substr(0, 1) !== "_") {
 +                target[key] = receiver;
 +            }
 +        }
 +    }
 +    
 +    return new Proxy(obj, handler);
 +}
 +</code>
 +
 +Vamos a usarlo:
 +
 +<code javascript>
 +let test = hidePrivates(obj);
 +
 +console.log(test._v); // undefined
 +test.v = 1200;
 +test._v = 100;
 +
 +console.log(obj._v); // 1200
 +</code>
 ===== Proxies a objetos de tipo función ===== ===== Proxies a objetos de tipo función =====
  
 +Ya hemos visto cómo con el método ''get'' podemos interceptar una llamada a una función del objeto envuelto por el proxy. Pero además existe otra posible intercepción y es cuando creamos un **proxy a una función**.
 +
 +Y es que, en JavaScript, las funciones son un tipo más de objeto, así que crear un proxy a una función no debe resultarte sorprendente.
 +
 +Con un proxy a una función podemos interceptar todas las llamadas a dicha función y ejecutar código antes y/o después de ejecutar la función envuelta por el proxy (aunque también podríamos optar por ni tan siquiera ejecutarla y devolver cualquier otro valor).
 +
 +Para crear un proxy a función en el handler se define el método apply. Este método recibe los siguientes parámetros:
 +
 +  * ''target'': Como en los otros casos contiene el objeto envuelto por el proxy
 +  * ''thisArg'': El valor de ''this'' usado
 +  * ''argumentsList'': Un array con los argumentos. Si no se pasan argumentos es un array vacío (no vale nunca ''undefined'').
 +
 +Veamos un ejemplo:
 +
 +<code javascript>
 +let foo = function(...values) {
 +    let result = 0;
 +    values.forEach(s => result += s);       
 +    console.log('Sum of values is ' + result);
 +}
 +
 +let proxy = new Proxy(foo, {
 +    apply(target, thisArg, argumentsList) {
 +        let args = argumentsList.map(arg => typeof(arg) === "number" ? arg : 0);
 +        return target.apply(thisArg, args);
 +    }
 +});
 +
 +proxy(10,20,"30");
 +</code>
 +
 +El proxy intercepta todas las llamadas a ''foo'' y sustituye por 0 todos los parámetros no numéricos. Es por ello que la llamada ''proxy(10, 20, "30")'' devuelve 30 (mientras que ''foo(10, 20, "30")'' devuelve "3030".
 +
 +==== Interceptando call, apply y bind ====
 +
 +El uso de ''call'' y ''apply'' se intercepta también de forma automática. Dado el mismo código anterior una llamada a ''proxy.call'' o ''proxy.apply'' es interceptada correctamente (y en este caso el parámetro ''thisArg'' toma el valor correspondiente):
 +
 +<code javascript>
 +proxy.apply([10], [10, 20, "30"]);
 +proxy.call([20], 10, 20, "30");
 +</code>
 +
 +En ambos casos se intercepta la llamada (se ejecuta el método ''apply'' del //handler//). En el primer caso el valor de ''thisArg'' es ''[10]'' y en el segundo es ''[20]''. En ambos casos ''argumentsList'' es ''[10, 20, "30"]'' (un array).
 +
 +Del mismo modo dado un proxy a una función, si usamos ''bind'' para obtener otra función atada permanentemente a otro valor de ''this'', las llamadas a esa otra función también son capturadas:
 +
 +<code javascript>
 +let proxy2 = proxy.bind("otherhis");
 +proxy(10, 20, "30");        // Se captura la llamada, thisArg vale "otherthis" (y devuelve 30 y no "3030").
 +</code>
 +
 +En resumen, con los proxies podemos interceptar llamadas a funciones de forma sencilla.
 +
 +Las posibilidades son muy amplias: validación de precondiciones y/o postcondiciones, auditorías de llamadas a funciones, enrutamiento dinámico de funciones... son escenarios que con ES5 ya se podían realizar, pero que requerían mucho más código y no eran, ni de lejos, tan transparentes ni sencillos como con los proxies.
 +
 +Vamos a ver a continuación un caso práctico de uso.
 ===== DEMO: Creando un "congelador" de objetos - Parte 1 ===== ===== DEMO: Creando un "congelador" de objetos - Parte 1 =====
  
 +Vamos a tener un objeto que nos permitirá generar proxies de objetos y poder congelar el objeto, que las llamadas a los //setters// no sean válidas.
 +
 +Empezaremos creando una clase que utilizaremos para obtener los proxies de un determinado objeto:
 +
 +<code javascript>
 +// Utilizaremos un WeakMap para guardar las variables privadas
 +// de los objetos de tipo Freezer
 +const _privates = new WeakMap();
 +
 +class Freezer {
 +
 +    constructor(obj) {
 +       let pr = {};
 +       pr.frozen = false; // el objeto está congelado?
 +       _privates.set(this, pr)
 +       pr.proxy = new Proxy(obj, {});
 +    }
 +    
 +    // Método para "congelar" o "descongelar" el objeto
 +    frozen(value) {
 +        let pr = _privates.get(this);
 +        pr.frozen = value ? true : false;
 +    }
 +    
 +    get value() {
 +        let pr = _privates.get(this);
 +        return pr.proxy;
 +    }
 +}
 +</code>
 +
 +Vamos a utilizarla:
 +
 +<code javascript>
 +var obj = {v: 42};
 +
 +var freezer = new Freezer(obj);
 +var proxy = freezer.value();
 +
 +obj.v = 100;
 +proxy; // Object { v: 100 }
 +</code>
 +
 +Ahora tendremos que añadir la funcionalidad para congelar el objeto.
 ===== DEMO: Creando un "congelador" de objetos - Parte 2 ===== ===== DEMO: Creando un "congelador" de objetos - Parte 2 =====
  
 +<code javascript>
 +// Utilizaremos un WeakMap para guardar las variables privadas
 +// de los objetos de tipo Freezer
 +const _privates = new WeakMap();
 +
 +const createConfig = function(pr) {
 +    return {
 +        set (target, key, receiver) {
 +            var prop = target[key];
 +            if (pr.frozen) {
 +                return prop.
 +            } else {
 +                target[key] = receiver;
 +                return receiver;
 +            }
 +        }
 +    }
 +}
 + 
 +class Freezer {
 + 
 +    constructor(obj) {
 +       let pr = {};
 +       pr.frozen = false; // el objeto está congelado?
 +       _privates.set(this, pr)
 +       pr.proxy = new Proxy(obj, {});
 +    }
 + 
 +    // Método para "congelar" o "descongelar" el objeto
 +    frozen(value) {
 +        let pr = _privates.get(this);
 +        pr.frozen = value ? true : false;
 +    }
 + 
 +    get value() {
 +        let pr = _privates.get(this);
 +        return pr.proxy;
 +    }
 +}
 +</code>
 +
 +Probamos:
 +
 +<code javascript>
 +var obj = {v:42};
 +
 +var freezer = new Freezer(obj);
 +var proxy = freezer.value;
 +
 +proxy; // Object { v: 42 }
 +
 +// Congelamos el objeto:
 +freezer.frozen(true);
 +
 +// Comprobemos que realmente está congelado:
 +
 +prop.v = 100;
 +
 +proxy.v; // 42
 +
 +// Para descongelarlo:
 +freezer.frozen(false);
 +
 +prop.v = 200;
 +proxy.v; // 200
 +</code>
 ===== DEMO: Creando un "congelador" de objetos - Parte 3 ===== ===== DEMO: Creando un "congelador" de objetos - Parte 3 =====
  
 +Después de lo anterior, vamos a agregar un método ficticio al proxy que llamándolo nos permita congelar el propio objeto.
 +
 +Queremos lograr hacer algo como ''proxy[Freezer.freeze](true);''.
 +
 +Vamos a ello:
 +
 +<code javascript>
 +// Utilizaremos un WeakMap para guardar las variables privadas
 +// de los objetos de tipo Freezer
 +const _privates = new WeakMap();
 +
 +// Para asegurarnos de que el nombre del método que crearemos
 +// en el proxy es único y no tenga conflictos, utilizaremos un símbolo
 +const _freeze = Symbol("freeze");
 + 
 +const createConfig = function(pr) {
 +    return {
 +        set (target, key, receiver) {
 +            var prop = target[key];
 +            if (pr.frozen) {
 +                return prop.
 +            } else {
 +                target[key] = receiver;
 +                return receiver;
 +            }
 +        },
 +        get (target, key, receiver) {
 +            if (key === _freeze) {
 +                return function(v) {
 +                    pr.frozen = v ? true : false;
 +                    return pr.frozen;
 +                }
 +            } else {
 +                return target[key];
 +            }
 +        }
 +    }
 +}
 + 
 +class Freezer {
 + 
 +    constructor(obj) {
 +       let pr = {};
 +       pr.frozen = false; // el objeto está congelado?
 +       _privates.set(this, pr)
 +       pr.proxy = new Proxy(obj, {});
 +    }
 + 
 +    // Método para "congelar" o "descongelar" el objeto
 +    frozen(value) {
 +        let pr = _privates.get(this);
 +        pr.frozen = value ? true : false;
 +    }
 + 
 +    get value() {
 +        let pr = _privates.get(this);
 +        return pr.proxy;
 +    }
 +    
 +    // getter para obtener el símbolo
 +    static get freeze() {
 +        return _freeze;
 +    } 
 +}
 +</code>
 +
 +Probamos:
 +
 +<code javascript>
 +var obj = {v:42};
 + 
 +var freezer = new Freezer(obj);
 +var proxy = freezer.value;
 +
 +// Congelamos:
 +proxy[Freezer.freeze](true); // true
 +
 +proxy.v = 100;
 +
 +proxy,.v; // 42
 +
 +obj.v; // 42
 +
 +// Descongelamos:
 +freezer.frozen(false);
 +
 +proxy.v = 100;
 +
 +proxy.v; // 100
 +obj.v; // 100
 +</code>
 +
 +===== Recursos =====
 +
 +  * [[https://developer.chrome.com/blog/es2015-proxies| Introducing ES2015 proxies]]
 +  * [[https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Extending_constructor|Extendiendo un constructor con un Proxy]] (MDN)
 +  * [[https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Proxy#manipulando_nodos_del_dom|Manipulando nodos del DOM]] (MDN)
informatica/programacion/cursos/programacion_avanzada_javascript/proxies.1730117362.txt.gz · Última modificación: por tempwin