函数属性——记忆模式(Memoization)

函数也是对象,所以它们可以有属性。事实上,函数也确实本来就有一些属性。比如,对一个函数来说,不管是用什么语法创建的,它会自动拥有一个length属性来标识这个函数期待接受的参数个数:

  1. function func(a, b, c) {}
  2. console.log(func.length); // 3

任何时候都可以给函数添加自定义属性。添加自定义属性的一个有用场景是缓存函数的执行结果(返回值),这样下次同样的函数被调用的时候就不需要再做一次那些可能很复杂的计算。缓存一个函数的运行结果也就是为大家所熟知的记忆模式。

在下面的例子中,myFunc函数创建了一个cache属性,可以通过myFunc.cache访问到。这个cache属性是一个对象(hash表),传给函数的参数会作为对象的key,函数执行结果会作为对象的值。函数的执行结果可以是任何的复杂数据结构:

  1. var myFunc = function (param) {
  2. if (!myFunc.cache[param]) {
  3. var result = {};
  4. // ……复杂的计算……
  5. myFunc.cache[param] = result;
  6. }
  7. return myFunc.cache[param];
  8. };
  9. // 缓存
  10. myFunc.cache = {};

上面的代码假设函数只接受一个参数param,并且这个参数是原始类型(比如字符串)。如果你有更多更复杂的参数,则通常需要对它们进行序列化。比如,你需要将arguments对象序列化为JSON字符串,然后使用JSON字符串作为cache对象的key:

  1. var myFunc = function () {
  2. var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)),
  3. result;
  4. if (!myFunc.cache[cachekey]) {
  5. result = {};
  6. // ……复杂的计算……
  7. myFunc.cache[cachekey] = result;
  8. }
  9. return myFunc.cache[cachekey];
  10. };
  11. // 缓存
  12. myFunc.cache = {};

需要注意的是,在序列化的过程中,对象的“标识”将会丢失。如果你有两个不同的对象,却碰巧有相同的属性,那么他们会共享同样的缓存内容。

前面代码中的函数名还可以使用arguments.callee来替代,这样就不用将函数名硬编码。不过尽管现阶段这个办法可行,但是仍然需要注意,arguments.callee在ECMAScript5的严格模式中是不被允许的:

  1. var myFunc = function (param) {
  2. var f = arguments.callee,
  3. result;
  4. if (!f.cache[param]) {
  5. result = {};
  6. // ……复杂的计算……
  7. f.cache[param] = result;
  8. }
  9. return f.cache[param];
  10. };
  11. // 缓存
  12. myFunc.cache = {};