underscore.js 中有一个 memoize 方法:

以下是代码片段:
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
  var memo = {};
  hasher = hasher || _.identity;
  return function() {
    var key = hasher.apply(this, arguments);
    return key in memo ? memo[key] : (memo[key] = func.apply(this, arguments));
  };
};
memoize 的原理很简单,实现时有以下注意点:

  1. 当 arguments 不是简单字符串时,如何有效构建 hasher 来生成唯一 key 值;
  2. 函数值的存储和获取;
  3. 当缓存数据很大时,如何根据访问频率来释放低访问量的缓存项。

第一个和第三个问题这里不谈。对于第二个问题,一般的实现方法是采用数组或对象来保存。比如 underscore.js 中的实现里,采用的是 memo = {}memo[key] 来实现存储和获取,这时要注意一个陷阱:

以下是代码片段:
var test = _.memoize(function(str) {
  return str;
});
alert(test(’hello’) == ’hello’); // true
alert(test(’toString’) == ’toString’); // false
test('toString') 返回的值是 ({}).toString.toString(), 这是由 key in memo 导致的。立刻修改为:

以下是代码片段:
// snip...
    return memo.hasOwnProperty(key) ? memo[key] : (memo[key] = func.apply(this, arguments));
// snip...
上面的代码看起来不会有 bug 了,但只要保持清澈明亮的眼睛,不难构造出以下用例:

以下是代码片段:
test(’hasOwnProperty’);
test(’throw exception’);
报错的原因是 memo.hasOwnPropery 已经不指向 Object.prototype.hasOwnProperty. 知道了问题所在,修改就不难了:

以下是代码片段:
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
  var memo = {};
  hasher = hasher || _.identity;
  return function() {
    var key = hasher.apply(this, arguments);
    return Object.prototype.hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
  };
};
注:此 bug 已提交给 underscore.js, 会很快修复。