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);
  })();
}