Skip to content

Latest commit

 

History

History
180 lines (122 loc) · 9.42 KB

File metadata and controls

180 lines (122 loc) · 9.42 KB

4.3 Fun with function as objects

随着我们持续讲解函数,我们会发现它和其他语言中的函数存在很多不同。javascript给了函数很多功能,而不仅仅是第一公民那么简单。

我们看到函数可以有属性,有方法,可以赋值给变量和属性,而且还具有“调用”的超能力。

本节中,我们将借助函数和其他对象之间的共同点来解决一些问题。在此之前,我们先来回顾一些之前的知识。

首先,把函数赋值给变量:

var obj = {};
var fn = function(){};
assert(obj && fn, "Both the object and function exist.");

注: fn赋值语句后面的分号是一种提倡做法。在js中我们最好以分号结束一行,尤其是在赋值语句中,函数赋值也不例外。在使用压缩方法时,结尾的分号会提升压缩效果,也能减少报错。

另外一种能力是,我们可以给函数属性:

var obj = {};
var fn = function(){};
obj.prop = "hitsuke (distraction)";
fn.prop = "tanuki (climbing)";

这种能力可以被广泛使用在库和页面代码的书写中,尤其是在回调管理的时候。让我们利用这个功能,来看看一些更有趣的事情。首先我们来看看怎么把函数存放在列表里,然后看看如何实现“函数记忆”技术。

4.3.1 Storing functions

有时候我们希望把不同的函数存放在集合中,同时防止重复存放,事件管理就是一个典型的例子。(我们将在13章深入讲解)此时我们的主要挑战就是如何判断函数已经被加入了。我们可以在使用的时候遍历这个集合,筛选出重复的函数,但是作为一个忍者,我们不仅要把事情做完就了事,更要把事情做好。

此时我们可以使用函数的属性来辅助判断,如下所示:

Listing 4.8 Storing a collection of unique functions

var store = {
  nextId: 1,                                        //#1 保存下一个可用的id
  cache: {},                                        //#2 函数缓存集合
  add: function(fn) {                               //#3
    if (!fn.id) {                                   //#3 保存到集合中
      fn.id = store.nextId++;                       //#3 同时保持uniq
      return !!(store.cache[fn.id] = fn);           //#3
    }                                               //#3
  }
};

function ninja(){}

assert(store.add(ninja),                            //#4
       "Function was safely added.");               //#4
assert(!store.add(ninja),                           //#4
       "But it was only added once.");              //#4

我们首先创建了一个仓库,它有2个属性,其中nextId用来保存下一个id,cache则是函数集合。在add函数中,我们利用函数的id属性来判断函数的是否重复添加,如果函数已经有了这个属性,那么他就已经加入了cache,如果没有,就给它创建一个id属性,然后给id赋值为nextId,同时nextId加1 。

tips:!!可以把js中的任何表达式转换成bool结果,比如 !!"he shot me down" == true和!!0 == false。在 listing4.8中,我们把函数转换成了bool值,当然我们可以硬编码,返回true,但是我们就没机会介绍!!了。

关于函数属性,我们需要撸起袖子学习的另一个技巧是,它可以令函数改变自己。这个技术可以用来保存函数之前计算出的结果,节省将来的计算时间。

4.3.2 Self-memoizing functions

函数备注,即构建一个可以记住自己之前计算值的函数。它可以减少不必要的重复计算,极大提高运行效率。

我们先看一个场景,其中我们保存了一些昂贵的计算结果,然后我们再来看看一个实际例子,其中我们保存了DOM元素方便将来查找。

MEMOIZING EXPENSIVE COMPUTATIONS

我们先来看看一个计算数字是否为质数的例子。该技术可以被用到很多其他更复杂的应用中,比如md5签名的计算,不过做例子有些太复杂了。

从外面看,它跟一般函数没太大区别,但是我们会增加一个“答案缓存”,将已经计算出的结果保存起来。如下:

Listing 4.9 Memoizing previously computed values

function isPrime(value) {
  if (!isPrime.answers) isPrime.answers = {};                  //#1 创建缓存
  if (isPrime.answers[value] != null) {                        //#2 有答案
    return isPrime.answers[value];                             //#2 则返回
  }                                                            //#2 答案
  var prime = value != 1; // 1 can never be prime
  for (var i = 2; i < value; i++) {
    if (value % i == 0) {
      prime = false;
      break;
    }
  }
  return isPrime.answers[value] = prime;                       //#3 返回是否质数
}

assert(isPrime(5), "5 is prime!" );                            //#5 测试
assert(isPrime.answers[5], "The answer was cached!" );         //#5 答案已经保存

在listing4.9中,如果最开始调用,则无answer属性,此时创建一个新的。如果value已定义,则说明答案已经缓存了,返回缓存内容。否则,才进入正式计算,最后把新结果缓存起来,方便下次使用。

这个方法有2个主要优势:

  • 性能明显提升
  • 用户无感知

同时,也有3点劣势:

  • 这是内存换性能的典型例子
  • 有人会坚持一个函数只做一个事,并且把它做好。把缓存和计算逻辑放到一起违背了该原则
  • 测试性能到底有多少提升并不容易

让我们再来看看另外一个例子。

MEMOIZING DOM ELEMENTS

利用tag名选择dom元素是一个常用操作,但是效率较低。我们可以利用刚刚发现的备注技术来优化性能,如下:

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

备注(缓存)方法非常简单,如果做一些简单测试,会发现性能大概提高了5倍。如table 4.1所示:

Table 4.1 All times are in ms for 100,000 iterations in a copy of Chrome 17

Code version Average Mininum Maximum Runs
非缓存 16.7 18 19 10
缓存 3.2 3 4 10

这么简单地技巧就提高了这么多性能,同时因为我们把缓存封装在函数内部,也没有对全局空间造成污染。

和其他对象一样拥有属性并不是函数的唯一超能力,函数的另一项能力就是他的上下文,下面我们就来看看。

4.3.3 Faking array methods

有时我们需要一个集合来保存一些对象,通常我们使用数组,但是有些时候,我们需要保存更多地状态,比如和集合相关的元数据。

一个选项可能是每次需要一个新集合的时候,都去创建一个新数组类,加入你需要的属性和方法。这种做法既繁琐又低效。

让我们看看有没有这种可能性,设计一个普通对象,然后给他加上我们需要的函数。既然数组对象的函数知道如何处理数组,我们是否可以让这些函数为我们的定制对象工作?

我们可以!如下:

Listing 4.10 Simulating array-like methods

var elems = {

  length: 0,                                                //#1 保存集合大小,这是模拟数组的第一步。

  add: function(elem){                                      //#2 实现add操作。既然内置的array对象已经有了类似方法,为什么要重复造轮子呢?
    Array.prototype.push.call(this, elem);
  },

  gather: function(id){                                     //#3 实现gather函数,先根据id找到对应元素,然后add到集合中
    this.add(document.getElementById(id));
  }
};

elems.gather("first");                                      //#4
assert(elems.length == 1 && elems[0].nodeType,              //#4 测试
       "Verify that we have an element in our stash");      //#4

elems.gather("second");                                     //#4
assert(elems.length == 2 && elems[1].nodeType,              //#4
       "Verify the other insertion");                       //#4

上例中,我们创建了一个普通对象,并且让他模仿了一个类似数组push的操作。首先我们像数组一样,需要定义一个length属性,然后我们定义了一个add函数,向集合中增加元素。我们没有自己写代码,而是使用了数组内置的push函数,来实现类似操作(先不要管prototype,我们会在第六章深入讲解面向对象。现在你只需要知道它是一个构造器保存class方法的仓库即可)。

正常情况下,Array.prototype.push()会通过函数上下文操作自己的Array对象, 但是这里,我们欺骗了push,通过call,让他把elems当做了自己的函数上下文。push函数会改变length大小,同时在对象上增加一个数字属性,它引用了被传入的对象。

我们分别定义了2个函数,add和gather:add函数调用array的push方法用来增加元素,gather在此基础上,先选择一个id指定的元素,然后把它加入对象。最后我们用gather进行了测试。

我们介绍的这种“邪恶”本领不仅展示了函数上下文的可塑性,同时也为引出下一章将讨论的函数参数列表的复杂性提供了铺垫。