Sei sulla pagina 1di 13

https://jherax.wordpress.

com/2015/02/24/js-lazy-function-definition/

Lazy Function
Definition Pattern

Éste
artículo tiene como fin dar a conocer uno de los patrones de diseño en
JavaScript, el cual ha demostrado ser muy eficiente en escenarios en donde
necesitamos inicializarobjetos, o hacer que una función ejecute una acción
sólo la primera vez que sea invocada, o almacenar resultados de operaciones
que se efectúan con cada llamado a la función (Memoization)
Una de las ventajas que logramos al implementar éste patrón, es conseguir un
mejor rendimiento ya que se puede reducir el costo computacional si
mantenemos los datos en cache (mediante un closure), y además podemos
lograr que sólo se carguen los objetos cuando sean requeridos (lazy load / lazy
evaluation)
Primero que todo vamos a repasar algunos conceptos que nos ayudarán a
entender mejor el patrón Lazy Function Definition.

CLOSURE
Un closure es un tipo especial de objeto que combina dos cosas: una función, y
el entorno en que se creó esa función. Es decir que una función definida dentro
del closure “recuerda” el entorno en el que se ha creado y tiene acceso a las
variables de ese entorno (el scope de la función padre).
Recordemos algunas propiedades de un closure (tomado del
artículo JavaScript: Closures)
 El closure permite encapsular el código.
 El contexto de una función anidada incluye el scope de la función externa.
 El entorno está formado por las variables locales dentro del ámbito ( scope)
cuando se creó el closure (variables libres).
 Una función anidada sigue teniendo acceso al contexto de la función
externa, incluso después de que ésta haya retornado.
Vamos a crear una función que rellene con ceros a la izquierda el texto
proporcionado y nos permita configurar el total de caracteres a ser retornados
por la función interna.
Lazy evaluation
Se conoce también como call-by-need y es una estrategia que retrasa la
evaluación de una expresión hasta que su valor es requerido evitando realizar
cálculos innecesarios. En ocasiones, el corto-circuito en la evaluación de
expresiones boleanas también es llamado lazy, en donde el segundo
argumento es evaluado sólo si el primer argumento resulta satisfactorio para el
operador lógico.
1 function test(nombre, edad) {
//lazy evaluation
2 if (nombre && edad && canVote(edad)) {
3 //ejecuta alguna tarea compleja
4 console.info("Ejecuta alguna tarea compleja");
5 }
}
6
7 function canVote(edad) {
8 //se realiza algún cálculo
9 console.log("%cEjecuta canVote()", "color: blue");
10 return edad > 17;
}
11
12
13

Lazy initialization
También conocido como inicialización perezosa o inicialización tardía, es
una técnica utilizada para retrasar la creación de un objeto, el cálculo de una
operación, o algún otro proceso costoso hasta que sea necesitado la primera
vez. La forma tradicional de lograr el objetivo, es mantener
un flag (indicador) que nos permita saber si el objeto es requerido.
1
2 function foo() {
3
//t es el flag que determina
4 //si el objeto fue requerido
5 if (foo.t) {
6 return foo.t;
7 }
8
9 console.log("inicializar");
foo.t = new Date();
10 return foo.t;
11 }
12

Memoization
La evaluación tardía es a menudo combinada con la técnica memoization, en
donde luego de calcular un valor según los parámetros de entrada de una
función, el resultado se almacena internamente en una tabla que es indexada
con los parámetros de la función; de manera que la próxima vez que se llame
a la función, primero se consulta la tabla para verificar si el valor ya fue
calculado anteriormente.
Recomiendo leer el artículo One-Line JavaScript Memoization de Oliver
Steele, el cual nos ayudará a comprender mejor este concepto.

LAZY FUNCTION DEFINITION


Primeramente debemos tener en cuenta que éste es un patrón de diseño con el
que conseguiremos reducir la ejecución de nuestro código haciendo que una
porción del código sea ejecutada solo una vez y de allí que recuerde el
resultado de una operación o el estado del closure.
Como todo patrón de diseño, debemos usarlo en el contexto adecuado, y una
función puede implementar el patrón de función perezosa si requiere
solucionar alguno de los siguientes problemas:
 la función requiere inicializar objetos.
 la función una vez evaluada, retornará el mismo valor en las próximas
llamadas.
 mantener el estado de objetos (closure) que serán utilizados por la función
principal.
Veamos un ejemplo tonto para ilustrar el concepto:

1
2 //declaramos una función
3 function pushButton() {
4
//redefinimos la función
5 pushButton = function() {
6 alert("pushButton() has been redefined");
7 };
8
9 //ésto será ejecutado sólo la primera vez
10 //que se invoque la función pushButton()
alert("First call to pushButton()");
11 }
12
13 pushButton(); //primer llamado
14 pushButton(); //segundo llamado
15
En el ejemplo anterior vemos que al ser llamada por primera vez, la función
ejecutó el alert de la línea 11, pero en los siguientes llamados siempre
ejecutará el alert de la línea 6, ésto ocurre porque en la línea 5 redefinimos el
objeto almacenado en la variable pushButton
Teniendo en cuenta que en JavaScript las funciones crean un scope de
privacidad, podemos crear objetos que determinen el estado interno de la
función que vamos a redefinir, veamos ésto con más claridad en el siguiente
ejemplo:
1 //declaramos una función
2 function pushButton() {
3
//mantenemos el estado interno
4 //de las veces que invoquemos la función
5 var _calls = 1;
6
7 //redefinimos la función
8 pushButton = function() {
_calls += 1;
9 alert(_calls + " call to pushButton()");
10 };
11
12 //ésto será ejecutado sólo la primera vez
13 //que se invoque la función pushButton()
14 alert("First call to pushButton()");
}
15
16 pushButton(); //primer llamado
17 pushButton(); //2
18 pushButton(); //3
19
20
21
De manera sencilla hemos visto lo que es una función que se sobrescribe así
misma. Sin embargo, en el patrón Lazy Function Definition, una función
perezosa esta conformada de tres partes:
1. Inicializar. Evaluar y hacer una serie de cálculos que determinan el valor a
retornar.
2. Redefinirse a sí misma. La función se sobrescribe a sí misma para evitar
realizar de nuevo las operaciones efectuadas en el paso anterior.
3. Autoinvocarse. La función se llama así misma para retornar el valor
después de sobrescribirse.
1
2 function foo() {
3
4 //1. inicializar
console.log("inicializar");
5
var t = new Date();
6
7 //2. sobrescribirse
8 console.log("sobrescribir");
9 foo = function() {
10 return t;
};
11
12 //3. autoinvocarse
13 console.log("autoinvocar");
14 return foo();
15 }
16
Hay escenarios en donde una función siempre va a retornar el mismo valor,
por ejemplo cuando necesitamos verificar las capacidades del navegador. En
este caso vamos a crear una función siguiendo el patrón Lazy Function
Definition para determinar la propiedad de texto de un nodo DOM:
1 var getText = function (DOMNode) {
2
3 //inicializar y redefinir condicionalmente
getText =
4 typeof DOMNode.innerText !== "undefined"
5 ? function (DOMNode) {
6 return DOMNode.innerText }
7 : function (DOMNode) {
return DOMNode.textContent };
8
9 //autoinvocar
10 return getText(DOMNode);
11 }
12
13
En el ejemplo anterior notamos que la función se sobrescribe dependiendo de
una condición, y como la condición sólo es evaluada la primera vez que se
invoque la función, entonces se define de acuerdo al resultado de esa
condición.

A tener en cuenta
Si bien con este patrón podemos tardar la inicialización de objetos hasta que
sean requeridos, esta ventaja tiene un precio, y el precio es que la función al
ser redefinida pierde la transparencia referencial y deja de ser un objeto de
primera clase.
Si quisiéramos pasar una función perezosa como argumento de otra función
(callback), o la queremos asignar como método de un objeto, cada vez que sea
invocada ejecutará siempre los tres pasos: inicializar, redefinirse, autoinvocarse,
así que la ganancia en velocidad y rendimiento se verá afectada. Para
solucionar este problema podemos optar por la inicialización perezosa (lazy
initialization) mediante un closure condicional:
1
2 //begin IIFE
3 var foo = (function() {
4 var some_complex;
5
return function() {
6 if (some_complex) {
7 return some_complex;
8 }
9
10 //lazy initialization
some_complex = new Date();
11 return some_complex;
12 }
13 })();
14 //end IIFE
15
A continuación vamos a trabajar un caso para demostrar porqué no se debe
utilizar una función perezosa como método de un objeto. Consideremos que
tenemos una función que recibe un número (1-12), y retorna un texto con el
mes correspondiente:
function getMonth(n) {
1
2 console.log("incializar");
3 var months = [
4 "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
5 "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre",
6 ];
7
console.log("redefinir");
8 getMonth = function(n) {
9 if (n < 1 || n > 12) {
10 throw new RangeError("Mes incorrecto");
}
11 return months[n - 1];
12 };
13
14 console.log("autoinvocar");
15 return getMonth(n);
}
16
17 console.info( getMonth(11) );
18 console.info( getMonth(12) );
19
20
21
22
Si ejecutamos esté código, podemos ver que en la consola sólo aparece los
textos “incializar”, “redefinir”, “autoinvocar” la primera vez que se ejecuta la
función. Hasta aquí nuestro patrón Lazy Function Definition funciona
perfectamente.

Caso #1
Ahora vamos a modificar el código para que la función getMonth sea un
método de un objeto (para crear el objeto vamos a utilizar el patrón Revealing
Module)
1 //REVEALING MODULE PATTERN
2 var mylib = (function() {
3
//LAZY FUNCTION DEFINITION PATTERN
4 function getMonth(n) {
5
6 console.log("incializar");
7 var months = [
8 "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
"Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre",
9
];
10
11 console.log("redefinir");
12 getMonth = function(n) {
13 if (n < 1 || n > 12) {
14 throw new RangeError("Mes incorrecto");
}
15 return months[n - 1];
16 };
17
18 console.log("autoinvocar");
19 return getMonth(n);
20 }
21
//API pública
22 return {
23 "getMonth": getMonth
24 };
25
26 }());
27
28 console.info( mylib.getMonth(5) );
console.info( mylib.getMonth(9) );
29
30
31
32
33
Si ejecutaron este código en la consola, podrán observar que en los dos
llamados al método se imprime los textos “incializar”, “redefinir”,
“autoinvocar”, lo que significa que cada vez que invoquemos el método éste
se inicializará de nuevo, perdiendo las virtudes que ofrecía el patrón Lazy
Function Definition. Ésto ocurre porque al ser redefinida la función pierde las
propiedades de ser un objeto de primera clase, y no va a conservar la misma
dirección de memoria cuando fue creada por primera vez, sino que al
sobrescribirse la función, un nuevo objeto es creado con una dirección de
memoria diferente. Veámoslo mejor en la siguiente gráfica.
Bueno, ¿y porqué entonces getMonth sí funciona cuando es creado como
una función, y en cambio no funciona cuando es asignado como método de un
objeto?
La respuesta es, porque al ser asignado como método de un objeto, éste
siempre va a conservar la referencia de la función cuando se creó por primera
vez, es decir que esa referencia no se va a actualizar cuando se sobrescriba la
función.

Caso #2
Lo mismo ocurre con el siguiente caso:

1
2
function getMonth(n) {
3
4 console.log("incializar");
5 var months = [
6 "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
7 "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre",
8 ];
9
console.log("redefinir");
10 getMonth = function(n) {
11 if (n < 1 || n > 12) {
12 throw new RangeError("Mes incorrecto");
13 }
return months[n - 1];
14 };
15
16 console.log("autoinvocar");
17 return getMonth(n);
18 }
19
20 var pointer = getMonth;
21
console.info( pointer(3) );
22 console.info( pointer(8) );
23
24
En esta ocasión sólo queríamos una “copia” de la función, pero al asignar la
función a otra variable, lo que realmente hacemos es pasar su referencia en
memoria, y al igual que en el caso #1, la variable pointer mantiene la
referencia de getMonth cuando se creó por primera vez y esa referencia no se
actualiza cuando getMonth se sobrescribe.

NOTA: Por eso es importante tener en cuenta no utilizar este patrón de


diseño cuando queremos asignar la función perezosa por referencia, ya sea
como callback, como método de un objeto, o simplemente asignarla a
otra variable!

Lazy Function como método de un objeto


Para efectos académicos voy a demostrar cómo podemos utilizar el
patrón Lazy Function Definition como método de un objeto, y vale la pena
conocerlo, ya que algunas librerías hacen una buena implementación de ésta
técnica, enfocada a sobrescribir métodos en la instancia de un objeto,
conservando el método original en el prototipo del constructor de dicho
objeto.
1
//define the method at prototype level
2 Geolocation.prototype.getNearSuggestions = function() {
3 var suggestions = []; //expensive computation
4
5 //override the method at instance level
6 this.getNearSuggestions = function() {
7 return suggestions;
};
8
9 return suggestions;
10 }
11
Ahora veamos el siguiente ejemplo: definiremos un método en el prototipo de
un objeto, y redefiniremos el método en la instancia del objeto, y una vez se
cumpla cierta condición, eliminaremos la referencia al método redefinido,
restableciendo el método original del prototipo.
1
2 //define the method at prototype level
3 System.prototype.download = function (file) {
4
5 //override the method at instance level
6 this.download = downloadInProgress;
7
8 //execute some actions
requestDownload(file, {
9 callback: function() {
10 //the condition is fulfilled,
11 //then deletes the method in the instance,
12 //restoring the method in the prototype
delete this.download;
13 }
14 });
15 }
16
17 function downloadInProgress() {
18 alert("still downloading...");
}
19
20
En los ejemplos anteriores, la palabra reservada this es la clave que nos
permite sobrescribir el método en la instancia actual y no en el prototipo.
Ahora vamos a modificar el código del caso #1 con el objetivo de lograr
implementar el patrón Lazy Function Definition como método público de un
objeto. No obstante les recuerdo que éste patrón de diseño no se debería usar
para exponer métodos de un objeto porque de no manejarse adecuadamente
podría ocasionar errores de lógica.
1
2
3 //Module pattern: Loose augmentation
4 var mylib = (function(context) {
5
6 //Lazy function definition
function getMonth(n) {
7
8 console.log("incializar");
9 var months = [
10 "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
11 "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre",
12 ];
13
console.log("redefine el método del objeto");
14 context.getMonth = function(n) {
15 if (n < 1 || n > 12) {
16 throw new RangeError("Mes incorrecto");
17 }
return months[n - 1];
18 };
19
20 console.log("invoca el método redefinido");
21 return context.getMonth(n);
22 }
23
24 //API pública
context.getMonth = getMonth;
25 return context;
26
27 }(mylib || {})); //loose augmentation
28
29 console.info( mylib.getMonth(5) );
30 console.info( mylib.getMonth(9) );
31
32
Si ejecutan el código en la consola, verán que a pesar de estar expuesto como
método de un objeto, el patrón Lazy Function Definition trabaja correctamente.
Para lograrlo, se utilizó el patrón Module: loose augmentation en el cual
pasamos como argumento el objeto mylib que será el módulo (líneas 3 y
28). Ahora dentro del módulo, en vez de usar this para referirnos al objeto,
emplearemos la variable context.
Para lograr que la función perezosa opere correctamente con un objeto,
debemos rescribir la propiedad del objeto que contiene la referencia a la
función original, es decir context.getMonth (línea 14) y retornamos el método
redefinido (línea 21).Finalmente asignamos la función original al objeto (línea
25) y retornamos el módulo.

Conclusión

El patrón Lazy Function Definition es una técnica que nos permite retrasar la
creación de objetos hasta cuando sean requeridos, permitiendo una ganancia
en el rendimiento de la aplicación, y como la función es redefinida por una
versión mas corta que accede a los objetos ya inicializados en el closure,
entonces la función perezosa se convierte en una versión óptima y más rápida
de la función original.
Éste es un patrón de diseño que vale la pena trabajar; cada vez que pensemos
en mejorar el desempeño de una función, podemos recurrir a él, y si de algún
modo queremos evitar el problema de referencia en memoria, podemos optar
por la solución de un closure condicional o utilizar el patrón Module: loose
augmentation para asegurar el contexto de ejecución de la función perezosa,
o redefinir el método en la instancia de un objeto, preservando el método
original en el prototipo.
Hay muchas formas en las que se puede aplicar este patrón de diseño, lo
importante es siempre tener en cuenta el contexto o problemas que soluciona,
sus limitaciones, y como cualquier patrón de diseño, puede ser combinado con
otros para construir soluciones más robustas.

Potrebbero piacerti anche