参考资料:

《JavaScript 高级程序设计(第三版)》 JavaScript专题之惰性函数 深入理解javascript函数进阶之惰性函数

因为不同厂商的浏览器相互之间存在一些行为上的差异,很多 js 代码包含了大量的if语句,将执行引导到正确的分支代码中去,比如下面的例子。

  1. function createXHR() {
  2. if (typeof XMLHttpRequest != 'undefined') {
  3. return new XMLHttpRequest();
  4. } else if (typeof ActiveXObject != 'undefined') {
  5. if (typeof arguments.callee.activeXString != 'string') {
  6. var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
  7. var i, len;
  8. for (i = 0, len = versions.length; i < len; i++) {
  9. try {
  10. new ActiveXObject(versions[i]);
  11. arguments.callee.activeXString = versions[i];
  12. } catch (e) {
  13. // skip
  14. }
  15. }
  16. }
  17. return new ActiveXObject(arguments.callee.activeXString);
  18. } else {
  19. throw new Error('No XHR object available.');
  20. }
  21. }

我们可以发现,在浏览器每次调用createXHR()的时候,它都要对浏览器所支持的能力仔细检查,但是很明显当第一次检查之后,我们就应该知道浏览器是否支持我们所需要的能力,因此除第一次之外的检查都是多余的。即使只有一个if语句也肯定要比没有if语句慢,所以if语句不必每次都执行,那么代码可以运行的更快一些,惰性载入就是用来解决这种问题的技巧。

函数重写

要理解惰性载入函数的原理,我们有必要先理解一下函数重写技术,由于一个函数可以返回另一个函数,因此可以在函数内部用新的函数来覆盖旧的函数。

  1. function sayHi() {
  2. console.info('Hi');
  3. sayHi = function() {
  4. console.info('Hello');
  5. }
  6. }

我们第一次调用sayHi()函数时,控制台会打印出Hi,全局变量sayHi被重新定义,被赋予了新的函数,从第二次开始之后的调用都会打印出Hello。惰性载入函数的本质就是函数重写,惰性载入的意思就是函数执行的分支只会发生一次。

惰性载入

我们来看一个例子(例子来源于冴羽所写的JavaScript专题之惰性函数)。现在需要写一个foo函数,这个函数返回首次调用时的Date对象,注意是首次。

方案一
  1. var t;
  2. function foo() {
  3. if (t) return t;
  4. t = new Date()
  5. return t;
  6. }
  7. // 此方案存在两个问题,一是污染了全局变量
  8. // 二是每次调用都需要进行一次判断

方案二
  1. var foo = (function() {
  2. var t;
  3. return function() {
  4. if (t) return t;
  5. t = new Date();
  6. return t;
  7. }
  8. })();
  9. // 使用闭包来避免污染全局变量,
  10. // 但是还是没有解决每次调用都需要进行一次判断的问题

方案三
  1. function foo() {
  2. if (foo.t) return foo.t;
  3. foo.t = new Date();
  4. return foo.t;
  5. }
  6. // 函数也是一种对象,利用这个特性也可以解决
  7. // 和方案二一样,还差一个问题没有解决

方案四
  1. var foo = function() {
  2. var t = new Date();
  3. foo = function() {
  4. return t;
  5. };
  6. return foo();
  7. };
  8. // 利用惰性载入技巧,即重写函数

惰性载入函数有两种实现方式,第一种是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一种按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行分支了。
第二种实现方式是在声明函数时就指定适当的函数。这样第一次调用时就不会损失性能了,而是在代码首次加载时会损失一点性能,即是利用闭包写一个自执行的函数。

改进 createXHR

有了上面的基础,我们就可以将createXHR()改进为下列形式,这样就不用每次调用都进行判断了。

  1. // 第一种实现方式
  2. function createXHR() {
  3. if (typeof XMLHttpRequest != 'undefined') {
  4. createXHR = function() {
  5. return new XMLHttpRequest();
  6. }
  7. } else if (typeof ActiveXObject != 'undefined') {
  8. createXHR = function() {
  9. if (typeof arguments.callee.activeXString != 'string') {
  10. var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
  11. var i, len;
  12. for (i = 0, len = versions.length; i < len; i++) {
  13. try {
  14. new ActiveXObject(versions[i]);
  15. arguments.callee.activeXString = versions[i];
  16. } catch (e) {
  17. // skip
  18. }
  19. }
  20. }
  21. return new ActiveXObject(arguments.callee.activeXString);
  22. };
  23. } else {
  24. createXHR = function() {
  25. throw new Error('No XHR object available.');
  26. }
  27. }
  28. }
  29. // 第二种实现方式
  30. function createXHR() {
  31. if (typeof XMLHttpRequest != 'undefined') {
  32. return function() {
  33. return new XMLHttpRequest();
  34. }
  35. } else if (typeof ActiveXObject != 'undefined') {
  36. return function() {
  37. if (typeof arguments.callee.activeXString != 'string') {
  38. var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
  39. var i, len;
  40. for (i = 0, len = versions.length; i < len; i++) {
  41. try {
  42. new ActiveXObject(versions[i]);
  43. arguments.callee.activeXString = versions[i];
  44. } catch (e) {
  45. // skip
  46. }
  47. }
  48. }
  49. return new ActiveXObject(arguments.callee.activeXString);
  50. };
  51. } else {
  52. return function() {
  53. throw new Error('No XHR object available.');
  54. }
  55. }
  56. }