总结《JavaScript 忍者秘籍》中函数相关的运用

匿名函数

匿名函数的介绍不用多说,通常,匿名函数的使用情况是:创建一个供以后使用的函数。

简单的举个例子如下:

window.onload = function() {
  alert('hello');
};
var templateObj = {
  shout: function() {
    alert('作为方法的匿名函数');
  }
};
templateObj.shout();

setTimeout(function() {
  alert('这也是一个匿名函数');
}, 1000);

递归

就是函数自调用,或者调用另外一个函数,但是函数调用树的某一处又重新调用了自己,就产生了递归

普通命名函数的递归

拿普通命名函数的递归最好的举例就是用最简单的递归需求:检测回文。

回文的定义:一个字符串,不管从哪一个方向读,结果一样。检测的工作有很多,我们可以创建一个函数,用待检测的回文字符逆序生成出一个字符,然后检测二者是否相同,如果相同,则为回文字符。

但是这种方法需要分配并创建新的字符,还有一种更简洁的方法:

  • 单个和零个字符都是回文
  • 如果字符串的第一个字符和最后一个字符相同,并且除了两个字符以外,别的字符也满足该要求,那么我们就可以检测出来了这个是回文了
function isPalindrome(txt) {
  if (txt.length <= 1) {
    return true;
  }
  if (txt.charAt(0) != txt.charAt(txt.length - 1)) return false;
  return isPalindrome(txt.substr(1, txt.length - 2));
}

方法中的递归

所谓的方法,自然离不开对象

var fn = {
  chirp: function(n) {
    return n > 1 ? fn.chirp(n - 1) + '-chirp' : 'chirp';
  }
};
console.log(fn.chirp(3)); //chirp-chirp-chirp

在上述代码中,我们通过对象 fn.chirp 方法的递归调用了自己。但是,因为我们在函数上用了非直接引用,也就是 fn 对象的 chirp 属性,所以才能够实现递归,这也就引出来一个问题:引用丢失

引用丢失的问题

上面的示例代码,依赖于一个进行递归调用的对象属性引用。与函数的实际名称不同,因为这种引用可能是暂时的。

var fn = {
  chirp: function(n) {
    return n > 1 ? fn.chirp(n - 1) + '-chirp' : 'chirp';
  }
};
var samurai = {
  chirp: fn.chirp
};

fn = {};

try {
  console.log(samurai.chirp(3) === 'chirp-chirp-chirp');
} catch (err) {
  if (err) alert(false);
}

// false

如上,执行结果会弹出 false,因为这时候 samurai.chirp 引用的同样是空对象,这就是引用丢失问题。

通过完善之前对匿名函数的粗略定义,我们可以修复解决这个问题。在匿名函数中,我们不在使用显式的 fn 引用。这里我们使用 this。

var fn = {
  chirp: function(n) {
    return n > 1 ? this.chirp(n - 1) + '-chirp' : 'chirp';
  }
};

当函数作为方法被调用的时候,函数的上下文指的是该方法的对象。

使用 this 调用,可以让我们的匿名函数更加的强大且灵活。

内联命名函数

上面我们解决了作为函数方法为递归时候的一个完美操作。其实这样写也还是有问题的,问题在于给对象定义方法的时候,方法名称是写死的,如果属性名称不一样,也一样会丢失引用。

这里我们采用另一种解决方案,给匿名函数起名

var fn = {
  chirp: function signal(n) {
    return n > 1 ? signal(n - 1) + '-chirp' : 'chirp';
  }
};
var samurai = {
  chirps: fn.chirp
};
fn = {};

try {
  console.log(samurai.chirps(3) === 'chirp-chirp-chirp');
} catch (err) {
  if (err) alert(false);
}

所以如上的解决办法,就完美解决了我们之前说到所有问题。内联函数还有一个很重要的一点,就是尽管可以给内联函数进行命名,但是这些名称只能在自身函数内部才可见。

将函数视为对象

JavaScript 中的函数和其他语言中的函数有所不同,JavaScript 赋予了函数很多的特性,其中最重要的特性之一就是函数作为第一类型对象。

所以,我们可以给函数添加属性,甚至可以添加方法。

函数存储

有时候,我们可能需要存储一组相关但又独立的函数,事件回调管理是最为明显的例子。向这个集合添加函数时候,我们得知道哪些函数在集合中存在,否则不添加。

var store = {
  nextId: 1,
  cache: {},
  add: function(fn) {
    if (!fn.id) {
      fn.id = store.nextId++;
      return !!(store.cache[fn.id] = fn);
    }
  }
};

function fn() {}

console.log(store.add(fn)); // true
console.log(store.add(fn)); // undefined

自记忆函数

缓存记忆是构造函数的过程,这种函数能够记住先前计算的结果。通过避免重复的计算,极大地提高性能。

缓存记忆昂贵的计算结果

作为一个简单的例子,这里我来判断一个数字是否为素数。

function isPrime(value) {
  if (!isPrime.answers) isPrime.answers = {};
  if (isPrime.answers[value] != null) {
    return isPrime.answers[value];
  }
  var prime = value != 1; //1 不是素数
  for (var i = 2; i < value; i++) {
    if (value % 2 === 0) {
      prime = false;
      break;
    }
  }
  return (isPrime.answers[value] = prime);
}
console.log(isPrime(5)); // true
console.log(isPrime.answers[5]); // true

可以通过下面的isPrime.answers[value]判断出缓存是否成功。

缓存记忆有两个主要的优点:

  • 在函数调用获取之前计算结果的时候,最终用户享有性能优势
  • 发生在幕后,完全无缝,最终用户和开发者都无需任何特殊的操作或者为此做任何初始化工作。

当然,也会有缺点:

  • 为了提高性能,任何类型的缓存肯定会牺牲内存
  • 纯粹主义者可能认为缓存这个问题不应该与业务逻辑放到一起。一个函数或者方法只应该做一件事。
  • 很难测试和测量一个算法的性能。(比如我们这个“简单”的例子)

缓存 DOM 记忆

通过元素标签名来获取 DOM 元素是一个非常常见的操作。但是性能可能不是特别好。所以从上面的缓存记忆我们可以进行如下的操作:

function getElements(name) {
  if (getElements.cache) getElements.cache = {};
  return (getElements.cache[name] =
    getElements.cache[name] || document.getElementsByTagName(name));
}

上面的代码很简单,而且这个简单的缓存的代码产生了 5 倍以上的性能提升。

我们可以将状态和缓存信息存储在一个封装的独立位置上,不仅在代码组织上有好处,而且外部存储或缓存对象无需污染作用域,就可以获取性能的提升。

伪造数组方法

有时候我们想创建一个包含一组数据的对象。如果只是集合,则只需要创建一个数组即可。但是在某些情况下,除了集合本身,可能会有更多的状体需要保存。

一种选择是,每次创建对象新版本的时候都创建一个新数组,然后将元数据作为属性或者方法添加到这个新数组上。但是这个操作太常规了。

欣赏如下骚操作:

<html>

<head></head>

<body>
  <input id="first">
  <input id="second">
  <script>
    var elems = {
      length: 0,
      add: function(elem) {
        Array.prototype.push.call(this, elem);
      },
      gather: function(id) {
        this.add(document.getElementById(id));
      }
    }
    elems.gather('first');
    console.log(elems.length, elems[0].nodeType);
    elems.gather('second');
    console.log(elems.length, elems[1].nodeType);
  </script>
</body>

</html>

通常,Array.prototype.push()是通过其函数上下文操作其自身数组的。这里我们通过 call 方法通过自己的对象代理了函数的上下文。push 的方法会增加 length 的值(会认为他就是数组的 length 属性),然后给对象添加一个数字属性,并将其引用到传入的元素上。

可变函数的参数列表

JavaScript 灵活且强大的特性之一是函数可以接受任意数量的参数。虽然 JavaScript 没有函数的重载,但是参数列表的灵活性是获取其他语言类似重载功能的关键所在

使用apply()支持可变参数

需求:查找数组中的最大值、最小值

一开始,我认为 Math 中提供的min(),max()可以满足,但是貌似他并不能够找到数组中的最大值最小值,难道:Math.min(arr[0],arr[1],arr[3]...)??

别闹了,来看看怎么做

function smallest(arr) {
  return Math.min.apply(Math, arr);
}

function largest(arr) {
  return Math.max.apply(Math, arr);
}

console.log(smallest([0, 1, 2, 3, 4])); // 0
console.log(largest([0, 1, 2, 3, 4])); // 4

函数重载

函数的隐式传递,arguments,也正是因为这个 arguments 的存在,才让函数有能力处理不同数量的参数。即使我们只定义固定数量的形参,通过 arguments 参数我们还是可以访问到实际传给函数的所有的参数。

检测并遍历参数

方法的重载通常是通过在同名的方法里声明不同的实例来达到目的。但是在 javascript 中并非如此,在 javaScript 中,我们重载函数的时候只有一个实现。只不过这个实现内部是通过函数实际传入的参数的特性和个数来达到相应目的的。

function merge(root) {
  for (var i = 1; i < arguments.length; i++) {
    for (var key in arguments[i]) {
      root[key] = arguments[i][key];
    }
  }
  return root;
}
var merged = merge(
  {
    name: 'Liusixin'
  },
  {
    age: 26
  },
  {
    city: 'Beijing'
  }
);
console.log(merged);

通过如上代码,我们将传递给函数的对象都合并到一个对象中。在 javascript 中,没有强制函数声明多少个参数就得传入多少个参数。函数是否可以成功处理这些参数,完全取决于函数本身的定义。

注意,我们要做的事情是想让第二个或者第 n 个参数上的属性合并到第一个对象中,所以这个遍历是从 1 开始的。

利用参数个数进行函数的重载

基于函数的参数,有很多种办法进行函数的重载。一种通用的方法是,根据传入参数的类型执行不同的操作。另一种办法是,可以通过某些特定参数是否存在来进行判断。还有一种是通过传入参数个数来进行判断。

假如对象上有一个方法,根据传入参数的个数来执行不同的操作,冗长且呆呆的函数应该张这样:

var fn = {
  whatever: function() {
    switch (arguments.length) {
      case: 0:
        //do something
        break;
      case: 1:
        //do something
        break;
      case: 2:
        //do something
        break;
      case: 3:
        //do something
        break;
    }
  }
}

这种方式,看起来非常的呆呆的。所以我们换一种方式来说下。

如果按照如下思路,添加重载的方法会怎样呢。

var fn = {};
addMethod(fn, 'whatever', function() {
  /*do something*/
});
addMethod(fn, 'whatever', function(a) {
  /*do something*/
});
addMethod(fn, 'whatever', function(a, b) {
  /*do something*/
});

这里我们使用同样的名称(whatever)将方法添加到该对象上,只不过每个重载的函数是单独的。注意每一个重载的函数参数是不同的。通过这种方式,我们真正为每一个重载都创建了一个独立的匿名函数。漂亮且简洁。

function addMethod(object, name, fn) {
  var old = object[name];
  object[name] = function() {
    if (fn.length === arguments.length) {
      return fn.apply(this, arguments);
    } else if (typeof old == 'function') {
      return old.apply(this, arguments);
    }
  };
}

首先我们保存原有的函数,针对传参个数做处理,避免不匹配。然后创建一个新的匿名函数,如果该匿名函数的形参和实参个数匹配,就调用这个函数,否则调用原来的函数。

这里的fn.length是返回函数定义时候定义的形参个数。

adMethod第一次调用会创建个新的匿名函数进行调用的时候将会调用这个 fn 函数。此时 fn 是一个新的对象,第二次调用addMethod的时候,会将之前的同名函数缓存到变量old中,然后将新创建的匿名函数作为方法。新方法首先检查传入的个数是否为 1,如果是则调用新传入的 fn,如果不是,则调用旧的。重新调用该函数的时候将在此检查参数个数是否为 0。

function addMethod(object, name, fn) {
  var old = object[name];
  object[name] = function() {
    if (fn.length === arguments.length) {
      return fn.apply(this, arguments);
    } else if (typeof old == 'function') {
      return old.apply(this, arguments);
    }
  };
}

var fn = {
  values: ['a', 'b', 'c', 'd']
};

addMethod(fn, 'find', function() {
  return this.values;
});

addMethod(fn, 'find', function(name) {
  var ret = [];
  for (var i = 0; i < this.values.length; i++) {
    if (this.values[i].indexOf(name) === 0) {
      ret.push(this.values[i]);
    }
  }
  return ret;
});

addMethod(fn, 'find', function(first, last) {
  var ret = [];
  for (var i = 0; i < this.values.length; i++) {
    if (this.values[i] == first + ' ' + last) ret.push(this.values[i]);
  }
  return ret;
});

console.log(fn.find().length);
console.log(fn.find('a'));
console.log(fn.find('a', 'c'));

然后使用如上的技巧的时候需要注意下面几点:

重载是适用于不同数量的参数,不区分类型、参数名称或者其他东西
这样的重载方法会有一些函数调用的开销。我们要考虑在高性能时的情况。