Последнее время меня увлекает изучать тонкие моменты ECMAScript по такому офигенному сборнику нюансов, как javascript garden. Пока я ковыряю язык, рождаются собственные js-велосипеды, которые мне грустно выбрасывать, поэтому возникло желание их тут выкладывать.

Одним из первых велосипедов была мемоизация произвольных функций с “каррированными” (отдельными для каждого из аргументов) кэшами:

var ControlFlow = {
  memoize: function(func) {
    if (typeof func !== 'function')
      throw "ArgumentError: function expected";

    var cacheRoot = {};
    var resultKey = {};

    return function() {
      var bag = cacheRoot;
      for (var i = 0; i < arguments.length; i++)
        if (arguments[i] in bag)
             bag = bag[arguments[i]];
        else bag = bag[arguments[i]] = {};
      return resultKey in bag
        ? bag[resultKey]
        : bag[resultKey] = func.apply(this, arguments);
    };
  }
}

Не смотря на некоторые грабли, возникшие при написании, гибкость и лаконичность языка всё же поражает. Проверим мемоизацию на любимом всеми факториале:

var Math = {
  fact: function(n) {
    console.log('fact('+n+')');
    if (n === 0) return 1;
    return n * Math.fact(n - 1);
  }
}

Использование (можно и не подменять исходную функцию):

Math.fact = ControlFlow.memoize(Math.fact);

Теперь вызовем разок:

Math.fact(4)
fact(4)
fact(3)
fact(2)
fact(1)
fact(0)
24

А теперь ещё раз:

Math.fact(6)
fact(6)
fact(5)
720

То есть для вычисления факториала 6 потребовалось только два рекурсивных вызова, так как значение fact(4) находится в кеше и не вычисляется заново. Аналогично поддерживаются функции произвольного количества аргументов:

var Math = {
  sum: ControlFlow.memoize(function() {
    // страшное преобразование arguments
    // в массив для последующего превращения в строку:
    console.log('sum(' +
      Array.prototype.slice.call(arguments).toString() + ')');

    var sum = 0;
    for (var i = 0; i < arguments.length; i++)
      sum += arguments[i];
    return sum;
  })
}

Usage:

Math.sum(2, 2)
sum(2,2)
4

Math.sum(2, 2)
4

Math.sum(1, 2, 3)
sum(1,2,3)
6

Math.sum(1, 2, 3)
6

Единственным нюансом является то, что при поиске по кэшу не учитывается параметр this функции. То есть функции всех экземпляров будут разделять один и тот же кэш. Дело в том, что обычно мемоизации подвергаются чистые функции, не замыкающиеся на какой-либо внешний (и в javascript всегда потенциально изменяемый) контекст. Это достаточно легко поправить и я оставлю реализацию на совести читателя.

p.s. Если я допускаю какие-либо распространнённые ошибки, то буду очень рад комментариям, ничего серъёзного на js не доводилось писать.