Lazy JS method evaluation
El otro día, mirando contra mi voluntad el código de CKEditor, me encontré con un patrón para evaluación lazy de los métodos de un objeto JS bastante canchero (y probablemente conocido).
Por ejemplo, digamos que un método de un objeto tiene una parte excesivamente
costosa, representada convenientemente por una función llamada
doSomethingExpensive
.
function doSomethingExpensive() {
console.log('expensive!')
return 42
}
function doSomethingCheap(answer) {
return answer + 1
}
function objeto() {}
objeto.prototype.metodo = function() {
var expensive = doSomethingExpensive()
doSomethingCheap(expensive)
}
var obj = new objeto()
obj.metodo() // logs 'expensive!'
obj.metodo() // logs 'expensive!' again
Bien, con cada llamada a metodo
se ejecuta doSomethingExpensive
. Si el
resultado de esa función varía con cada llamada al método, no hay mucho que
hacer. Pero si el resultado puede cachearse, o si es necesario porque el
resultado necesita ser compartido entre sucesivas llamadas al método, entonces
una primera forma de cambiarlo sería procesarlo cuando se declara el método:
objeto.prototype.metodo = (function() {
var expensive = doSomethingExpensive()
return function() {
doSomethingCheap(expensive)
}
})()
var obj = new objeto() // logs 'expensive!'
obj.metodo() // no loguea nada
obj.metodo() // no loguea nada
La
IIFE
se ejecuta en el momento de declarar el método, evalúa doSomethingExpensive
,
almacena el resultado y devuelve una función que usa ese valor almacenado. Esto
es un avance, pero presenta la desventaja de que ejecuta doSomethingExpensive
incluso si metodo
nunca se llama. Dependiendo del caso, esto puede ser
importante, ya sea que se vayan a inicializar muchos objetos como para retrasar
el tiempo de inicio de la aplicación, o porque el resultado de
doSomethingExpensive
solamente es significativo en el momento en que se
ejecuta metodo
, no en el momento en el que se declara. En ese caso, la
alternativa, que es la que vi en el código de CKEditor (en particular
acá
al momento de escribir esto, el método getWindow
de document
), es que el
método haga la evaluación, y luego se reemplace a sí mismo por una copia que use
el valor ya calculado:
objeto.prototype.metodo = function() {
var expensive = doSomethingExpensive()
this.metodo = function() {
return doSomethingCheap(expensive)
}
return this.metodo()
}
var obj = new objeto() // no loguea nada
obj.metodo() // logs 'expensive!'
obj.metodo() // no loguea nada
Esto combina lo mejor de los dos mundos, no ejecuta doSomethingExpensive
si
metodo
no se ejecuta nunca, y por otro lado si lo hace, lo hace una sola vez y
comparte el resultado entre las sucesivas llamadas a metodo
. Aprovechando que
el valor de retorno de una asignación es el valor asignado, se puede hacer una
versión más corta:
objeto.prototype.metodo = function() {
var expensive = doSomethingExpensive()
return (this.metodo = function() {
return doSomethingCheap(expensive)
})()
}