浅谈自回忆函数【分分快三计划】

作者:分分快三计划

总结

自记忆函数有两个优点:

  • 由于函数调用时会寻找之前调用所得到的值,所以用户最终会乐于看到所获得的性能收益。
  • 它不需要执行任何特殊请求,也不需要做任何额外初始化,就能顺利进行工作。

但是,自记忆函数并不是完美的,它一样有着缺陷:

  • 任何类型的缓存都必然会为性能牺牲内存。
  • 很多人认为缓存逻辑不应该和业务逻辑混合,函数或方法只需要把一件事情做好。
  • 对自记忆函数很难做负载测试或估算算法复杂度,因为结果依赖于函数之前的输入。

函数存储

利用以下代码可以完成函数存储功能。

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

使用场景:可以用来存储事件的回调函数,由于在 addEventListenerattachEvent 的解绑过程中都需要原样传入绑定的函数,所以我们可以将绑定的函数存储下来,以供解绑事件时使用。

本文包括以下内容:

简介

何为自记忆函数?书中提到:

记忆化(memoization)是一种构建函数的处理过程,能够记住上次计算结果

通过这句话可以得出,自记忆函数其实就是能够记住上次计算结果的函数。在实现中,我们可以这样进行处理:当函数计算得到结果时,就将该结果按照参数存储起来。采取这种方式时,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果而不是再计算一遍。

显而易见,像这样避免既重复又复杂的计算可以显著提高性能。对于动画中的计算、搜索不经常变化的数据或任何耗时的数学计算来说,记忆化这种方式是十分有用的。


在本文这一部分讨论JavaScript基础时,也许你会感到惊讶,我们的第一个论点是函数(function)而非对象(object)。当然,第3部分会用大量笔墨解释对象,但归根结底,你要理解一些基本事实,像普通人一样编写代码和像“忍者”一样编写代码的最大差别在于是否把JavaScript作为函数式语言(functional language)来理解。对这一点的认知水平决定了你编写的代码水平。

一个自记忆函数的例子

下面这个例子展现自记忆函数的工作方式:

// 自记忆素数检测函数
function isPrime (value) {
  // 创建缓存
  if (!isPrime.answers) {
    isPrime.answers = {};
  }
  // 检查缓存的值
  if (isPrime.answers[value] !== undefined) {
    return isPrime.answers[value];
  }
  // 0和1不是素数
  var prime = value !== 0 && value !== 1;
  // 检查是否为素数
  for (var i = 2; i < value; i  ) {
    if (value % i === 0) {
      prime = false;
      break;
    }
  }
  // 存储计算值
  return isPrime.answers[value] = prime
}

isPrime函数是一个自记忆素数检测函数,每当它被调用时:

首先,检查它的answers属性来确认是否已经有自记忆的缓存,如果没有,创建一个。

接下来,检查参数之前是否已经被缓存过,如果在缓存中找到该值,直接返回缓存的结果。

如果参数是一个全新的值,进行正常的素数检测。

最后,存储并返回计算值。

伪造数组

出于某种目的(我也不知道),我们可以将对象伪造成一个数组,具体操作如下:

var eles = {
    length: 0,
    add: function(ele) {
        Array.prototype.push.call(this, ele);
    }
};

eles 对象添加了一个 length 属性,当调用 push 方法时,length 属性会自动增加。

如果你正在阅读这本文,那么你应该不是一位初学者。对于后续内容,我们假设你已经足够了解面向对象基础(当然,我们会在以后章节详细讨论对象的高级概念),但真正理解JavaScript中的函数才是你能使用的唯一一件重要武器。函数是如此重要,所以本文及接下来两章将带领你彻底理解JavaScript中的函数。

浅谈自记忆函数

最近阅读《JavaScript忍者秘籍》看到了一种有趣的函数:自记忆函数。

函数判断

一般而言,要判断一个函数类型,只需要利用 typeof functionName 即可(会返回字符串 function)。不过会有一些特殊情况让我们的判断失效,比如下面几种:

  1. Opera: 在 HTML<object> 元素上使用 typeof的话,会返回 function,而不是我们期望的 object。(书中说在 Firefox 中会出现这个问题,不过我亲自检测之后,发现我电脑上的 Firefox并没有出现上述问题,反而是 Opera 出现了这个问题 )。

  2. Safari: Safari 认为 DOMNodeList 是一个 function,所以 typeof document.body.childNodes == function。(本人未亲自尝试)

基于以上情况,我们需要寻找一种完美的解决方案,不过事实上并不存在完美的解决方案,倒是有一种接近完美的方案,那就是利用 Object.toString() 方法。代码如下:

function isFunction(fn) {
    return Object.prototype.toString.call(fn) === "[object Function]";  
}

利用这项技术,还可以判断 String, RegExp, Date等其它对象。

这里我们不直接调用 fn.toString() 的原因有两个:

  1. 不同的对象可能有自己的 toString() 方法实现。

  2. JavaScript 中的大多数类型都已经有一个预定义的 toString() 方法覆盖了 Object.prototype 提供的 toString() 方法。

从下面可以看出 StringArray 重写了 ObjecttoString() 方法。

var sContent = "Hello World";
console.log(sContent.toString());   // "Hello World"

var aContent = [1, 2, 3];
console.log(aContent.toString());   // "[1, 2, 3]" 

刚才已经提及,上面这个检测的方法只是接近完美,这说明它也有失误的情况,比如在 IE 中会将 DOM 元素的方法报告成 object

本文摘自《JavaScript忍者秘籍(第2版)》

分分快三计划 1

image

《JavaScript忍者秘籍 第2版》

[美] John,Resig(莱西格),Bear,Bibeault(贝比奥特),Josip ... 著

点击封面购买纸书

JavaScript 正以惊人的速度成为各种应用程序的通用语言,包括 Web、桌面、云和移动设备上的应用程序。当成为 JavaScript 专业开发者时,你将拥有可应用于所有这些领域的、强大的技能集。

《JavaScript 忍者秘籍(第2版)》使用实际的案例清晰地诠释每一个核心概念和技术。本书向读者介绍了如何掌握 JavaScript 核心的概念,诸如函数、闭包、对象、原型和 promise,同时还介绍了 JavaScript API, 包括 DOM、事件和计时器。你将学会测试、跨浏览器开发,所有这些都是高级JavaScript开发者应该掌握的技能。

小福利

关注【异步社区】服务号,转发本文至朋友圈或 50 人以上微信群,截图发送至异步社区服务号后台,并在文章底下留言,分享你的JavaScript开发经验或者本书的试读体验,我们将选出3名读者赠送《JavaScript 忍者秘籍(第2版)》1本,赶快积极参与吧!
活动截止时间:2018 年3月15日

分分快三计划 2

image

在“异步社区”后台回复“关注”,即可免费获得2000门在线视频课程;推荐朋友关注根据提示获取赠书链接,免费得异步图书一本。赶紧来参加哦!

扫一扫上方二维码,回复“关注”参与活动!

阅读原文,购买《JavaScript 忍者秘籍(第2版)》

阅读原文

自记忆函数

所谓自记忆函数,就是说函数自己能够记住先前计算的结果,这样就能避免相同的计算执行两次,可以显著的提高性能。比如说下面这个检测是否为素数的函数。

    function isPrime(value) {
        if (!isPrime.results) {
            isPrime.results = {};
        }
        if (isPrime.results[value] !== undefined) {
            return isPrime.results[value];
        }
        var prime = value !== 1;
        for(var i = 2; i < value; i  ) {
            if (value % i === 0) {
                prime = false;
                break;
            }
        }
        return isPrime.results[value] = prime;
    }

缓存记忆有两个优点:

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

将缓存记忆用在 DOM 的获取操作上,可以获得 5 倍的性能提升,如下所示。

function getElements(name) {
    if (!getElements.cache) {
        getElements.cache = {};
    }

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

上面我们求素数的例子中,其实是在函数中对结果进行了缓存,不过值得注意的一点是,这种实现只有在我们能获取到函数体的时候才可以使用。下面我们就对上面的函数进行改写。

Function.prototype.memoized = function(key) {
    this._values = this._values || {};

    return this._values[key] !== undefined ?
        this._values[key] :
        this._values[key] = this.call(this, key);
};

function isPrime(num) {
    var prime = num != 1;

    for(var i = 2; i < num; i  ) {
        if (num % i === 0) {
            prime = false;
            break;
        }
    }

    return prime;
}

console.log(isPrime.memoized(5));
console.log(isPrime._values[5]);

这种写法可以解决刚才我们提出的无法获取函数体的问题,不过又出现了一个问题,因为上面的函数要求调用者在使用 isPrime() 的时候必须要跟上 .memoized(),不过调用者不可能时刻都能记得这一点,所以对于这个函数我们还可以改写,如下所示:

Function.prototype.memoized = function(key) {
    this._values = this._values || {};

    return this._values[key] !== undefined ?
        this._values[key] :
        this._values[key] = this.call(this, key);
};

Function.prototype.memoize = function(key) {
    var fn = this;
    return function() {
        return fn.memoized.call(fn, key);
    }
};

var isPrime = (function(num) {
    var prime = num !== 1;

    for(var i = 2; i < num; i  ) {
        if (num % i === 0) {
            prime = false;
            break;
        }
    }

    return prime;
}).memoize();

console.log(isPrime(5));

不过,上面的功能都被添加在 Function 上,由所有函数实例共享,如果感觉这么做有所不妥,可以使用下面这种方式。

function memoize(fn) {
    var cache = {};

    return function(key) {
        console.log("before: "   cache[key]);
        return cache[key] !== undefined ?
            cache[key] :
            cache[key] = fn.call(this, key);
    }
}

只需要对要缓存的函数进行包装即可。

使用比较器排序

一般情况下只要我们拿到了一组数据集,就很可能需要对它进行排序。假如有一组随机序列的数字数组:0, 3, 2, 5, 7, 4, 8, 1。也许这个顺序没什么问题,但很可能早晚需要重新排列它。

通常来说,实现排序算法并不是编程任务中最微不足道的;我们需要为手中的工作选择最佳算法,实现它以适应当前的需要(使这些选项是按照特定顺序排列),并且需要小心仔细不能引入故障。除此之外,唯一特定于应用程序的任务是排列顺序。幸运的是,所有的JavaScript数组都能用sort方法。利用该方法可以只定义一个比较算法,比较算法用于指示按什么顺序排列。

这才是回调函数所要介入的!不同于让排序算法来决定哪个值在前哪个值在后,我们将会提供一个函数来执行比较。我们会让排序算法能够获取这个比较函数作为回调,使算法在其需要比较的时候,每次都能够调用回调。该回调函数的期望返回值为:如果传入值的顺序需要被调换,返回正数;不需要调换,返回负数;两个值相等,返回0。对于排序上述数组,我们对比较值做减法就能得到我们所需要的值。

1<pre class="代码无行号"><code>var values = [0, 3, 2, 5, 7, 4, 8, 1];
2values.sort<strong>(function(value1, value2) {</strong>
3 return value1 - value2;
4<strong>}</strong>);</code></pre>

没有必要思考排序算法的底层细节(甚至是选择了什么算法)。JavaScript引擎每次需要比较两个值的时候都会调用我们提供的回调函数。

函数式方式让我们能把函数作为一个单独实体来创建,正像我们对待其他类型一样,创建它、作为参数传入一个方法并将它作为一个参数来接收。函数就这样显示了它一等公民的地位。

本文中的内容来自于 《JavaScript 忍者秘籍》。

1.3.1 函数声明和函数表达式

JavaScript中定义函数最常用的方式是函数声明和函数表达式。这两种技术非常相似,有时甚至难以区分,但在后续章节中你将看到,它们之间还是存在着微妙的差别。

  • 理解函数为何如此重要

  • 函数为何是第一类对象

  • 定义函数的方式

  • 参数赋值之谜

在这个代码清单中,我们使用自定义函数report(在本文附录B中定义)来输出代码执行过程中的信息,这样一来我们就能通过这些信息来跟踪程序的执行过程。我们还使用了第1章中的断言函数assert。该函数通常使用两个参数。第一个参数是用于断言的表达式。本例中,我们需要确定使用参数getText调用useless函数返回的值与变量text是否相等(useless(getText)

text)。若第一个参数的执行结果为true,断言通过;反之,断言失败。第二个参数是与断言相关联的信息,通常会根据通过/失败来输出到日志上。(附录B中概括地探讨了测试,以及我们对assert函数和report函数的简单实现)。

这段代码执行完毕后,执行结果如图1.1所示。可以看到,使用getText参数调用useless回调函数后,得到了期望的返回值。

分分快三计划 3

image

图1.1 清单1.1中代码的执行结果

我们还可以看看这个简单的回调函数具体是如何执行的。如图1.2所示,getText函数作为参数传入了useless函数。从该图中可以看到,在useless函数体内,通过callback参数可以取得getText函数的引用。随后,回调函数callback()的调用让getText函数得到执行,而我们作为参数传入的getText函数则通过useless函数被回调。

分分快三计划 4

image

图1.2 执行useless(getText)调用后的执行流。getText作为参数传入useless函数并调用。useless函数体内对传入函数进行调用,本例中触发了getText函数的执行(即我们对getText函数进行回调)。

完成这个过程是很容易的,原因就在于JavaScript的函数式本质让我们能把函数作为第一类对象。更进一步说,我们的代码可以写成如下形式:

1<pre class="代码无行号"><code>var text = 'Domo arigato!';
2function useless(ninjaCallback) {
3 return ninjaCallback();
4}
5assert(useless(<strong>function () { return text;}</strong>) === text,  ?--- 直接以参数形式定义回调函数
6    "The useless function works! " text); </code></pre>

JavaScript的重要特征之一是可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)。当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必须的名字污染全局命名空间。

在回调函数的前述例子中,我们调用的是我们自己的回调。除此之外浏览器也会调用回调函数,回想一下第2章中的下述例子:

1document.body.addEventListener("mousemove", function() {
2
3 var second = document.getElementById("second")
4;
5 addMessage(second, "Event: mousemove"
6);
7});

上例同样是一个回调函数,作为mousemove事件的事件处理器,当事件发生时,会被浏览器调用。

{注意 }

本小节介绍的回调函数是其他代码会在随后的某个合适时间点“回过来调用”的函数。你已经学习了我们自己的代码调用回调(useless函数例子),也看到了当某事件发生时浏览器发起调用(mousemove例子)。注意这些很重要,不同于我们的例子,一些人认为回调会被异步调用,因此第一个例子不是一个真正的回调。这里之所以提到这些是以防万一你偶尔会遇见这类激烈的争论。

现在让我们看一个回调函数的用法,它能极大地简化集合的排序。

让我们通过了解函数式编程的优点来开始学习吧。

1.2 函数作为对象的乐趣

本节我们会考察函数和其他对象类型的相似之处。也许让你感到惊讶的相似之处在于我们可以给函数添加属性:

1var ninja = {};
2ninja.name = "hitsuke";  ?--- 创建新对象并为其分配一个新属性
3var wieldSword = function(){};
4wieldSword.swordType = "katana";  ?--- 创建新函数并为其分配一个新属性

我们再来看看这种特性所能做的更有趣的事:

  • 在集合中存储函数使我们轻易管理相关联的函数。例如,某些特定情况下必须调用的回调函数。

  • 记忆让函数能记住上次计算得到的值,从而提高后续调用的性能。

让我们行动起来吧。

1.1 函数式的不同点到底是什么

函数及函数式概念之所以如此重要,其原因之一在于函数是程序执行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段执行的,我们编写的所有的脚本代码都将在一个函数内执行。

由于我们的大多数代码会作为函数调用来执行,因此,我们在编写代码时,通用强大的构造器能赋予代码很大的灵活性和控制力。本文的大部分内容解释了如何利用函数作为第一类对象的特性获益。首先浏览一下对象中我们能使用的功能。JavaScript中对象有以下几种常用功能。

  • 对象可通过字面量来创建{}。

  • 对象可以赋值给变量、数组项,或其他对象的属性。

1var ninja = {};  ?--- 为变量赋值一个新对象
2ninjaArray.push({});   ?--- 向数组中增加一个新对象
3ninja.data = {};  ?--- 给某个对象的属性赋值为一个新对象

  • 对象可以作为参数传递给函数。

1function hide(ninja){
2  ninja.visibility = false;
3}
4hide({});  ?--- 一个新创建的对象作为参数传递给函数

  • 对象可以作为函数的返回值。

1function returnNewNinja() {
2 return {};  ?--- 从函数中返回了一个新对象
3}

  • 对象能够具有动态创建和分配的属性。

1var ninja = {};
2ninja.name = "Hanzo";  ?--- 为对象分配一个新属性

其实,不同于很多其他编程语言,在JavaScript中,我们几乎能够用函数来实现同样的事。

1.2.2 自记忆函数

如同前面所提到的,记忆化(memoization)是一种构建函数的处理过程,能够记住上次计算结果。在这个果壳里,当函数计算得到结果时就将该结果按照参数存储起来。采用这种方式时,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果而不是再计算一遍。像这样避免既重复又复杂的计算可以显著地提高性能。对于动画中的计算、搜索不经常变化的数据或任何耗时的数学计算来说,记忆化这种方式是十分有用的。

看看下面的这个例子,它使用了一个简单的(也的确是效率不高的)算法来计算素数。尽管这是一个复杂计算的简单例子,但它经常被应用到大计算量的场景中(例如可以引申到通过字符串生成MD5算法),这里不便展示。

从外表来说,这个函数和任何普通函数一样,但在内部我们会构建一个结果缓存,它会保存函数每次计算得到的结果,如清单1.3所示。

清单1.3 计算先前得到的值

1function isPrime(value) {
2 if (!isPrime.answers) {
3  isPrime.answers = {};
4 }  ?--- 创建缓存
5 if (isPrime.answers[value] !== undefined) {
6  return isPrime.answers[value];
7 }  ?--- 检查缓存的值
8 var prime = value !== 0 && value !== 1; // 1 is not a prime
9 for (var i = 2; i < value; i ) {
10   if (value % i === 0) {
11    prime = false;
12    break;
13   }
14 }
15 return isPrime.answers[value] = prime;   ?--- 存储计算的值
16}
17assert(isPrime(5), "5 is prime!");
18assert(isPrime.answers[5], "The answer was cached!");   ?--- 测试该函数是否正常工作

在isPrime函数中,首先通过检查它的answers属性来确认是否已经创建了一个缓存,如果没有创建,则新建一个:

1if (!isPrime.answers) {
2  isPrime.answers = {};
3}

只有第一次函数调用才会创建这个初始空对象,之后这个缓存就已经存在了。然后我们会检查参数中传的值是否已经存储到缓存中:

1if (isPrime.answers[value] !== undefined) {
2 return isPrime.answers[value];
3}

这个缓存会针对参数中的值value来存储该值是否为素数(true或false)。如果我们在缓存中找到该值,函数会直接返回。

1return isPrime.answers[value] = prime;

这个缓存是函数自身的一个属性,所以只要该函数还存在,缓存也就存在。

最后的测试结果可以看到记忆函数生效了。

1assert(isPrime(5), "5 is prime!");
2assert(isPrime.answers[5], "The answer was cached!");

这个方法具有两个优点。

  • 由于函数调用时会寻找之前调用所得到的值,所以用户最终会乐于看到所获得的性能收益。

  • 它几乎是无缝地发生在后台,最终用户和页面作者都不需要执行任何特殊请求,也不需要做任何额外初始化,就能顺利进行工作。

当然这种方法并不是像玫瑰和提琴一样完美,还是要权衡利弊。

  • 任何类型的缓存都必然会为性能牺牲内存。

  • 纯粹主义者会认为缓存逻辑不应该和业务逻辑混合,函数或方法只需要把一件事做好。但不必担心,在第8章你会了解到如何解决这类问题。

  • 对于这类问题很难做负载测试或估计算法复杂度,因为结果依赖于函数之前的输入。

现在你看到了函数作为第一类公民的一些实例,接下来看看不同的函数定义的方式。

JavaScript中最关键的概念是:函数是第一类对象(first-class objects),或者说它们被称作一等公民(first-class citizens)。函数与对象共存,函数也可以被视为其他任意类型的JavaScript对象。函数和那些更普通的JavaScript数据类型一样,它能被变量引用,能以字面量形式声明,甚至能被作为函数参数进行传递。本文一开始会介绍面向函数编程带来的差异,你会发现,在需要调用某函数的位置定义该函数,能让我们编写更紧凑、更易懂的代码。其次,我们还会探索如何把函数用作第一类对象来编写高性能函数。你能学到多种不同的函数定义方式,甚至包括一些新类型,例如箭头(arrow)函数,它能帮你编写更优雅的代码。最后,我们会学习函数形参和函数实参的区别,并重点关注ES6的新增特性,例如剩余参数和默认参数。

1.1.2 回调函数

每当我们建立了一个将在随后调用的函数时,无论是在事件处理阶段通过浏览器还是通过其他代码,我们都是在建立一个回调(callback)。这个术语源自于这样一个事实,即在执行过程中,我们建立的函数会被其他函数在稍后的某个合适时间点“再回来调用”。

有效运用JavaScript的关键在于回调函数,相信你已经在代码中使用了很多回调函数——不论是单击一次按钮、从服务端接收数据,还是UI动画的一部分。

本节我们会看一些实际使用回调函数的典型例子,例如处理事件、简单的排序集合。这部分内容会有点复杂,所以在深入学习之前,先透彻、完整地理解回调函数的概念,用最简单的形式来展现它。下面我们用一个简单例子来阐明这个概念,此例中的函数完全没什么实际用处,它的参数接收另一个函数的引用,并作为回调调用该函数:

1function useless(ninjaCallback) {
2 return ninjaCallback();
3}

这个函数可能没什么用,但它反映了函数的一种能力,即将函数作为另一个函数的参数,随后通过参数来调用该函数。

我们可以在清单1.1中测试一下这个名为useless的函数。

清单1.1 简单的回调函数例子

1var text = "Domo arigato!";
2report("Before defining functions");
3function useless(ninjaCallback) {
4  report("In useless function");
5  return ninjaCallback();
6}  ?--- 函数定义,参数为一个回调函数,其函数体内会立即调用该回调函数
7function getText() {
8 report("In getText function");
9 return text;
10}  ?--- 简单的函数定义,仅返回一个全局变量
11report("Before making all the calls");
12assert(useless(getText) === text,
13     "The useless function works! " text);   ?--- 把gerText作为回调函数传入上面的useless函数
14report("After the calls have been made");

1.1.1 函数是第一类对象

JavaScript中函数拥有对象的所有能力,也因此函数可被作为任意其他类型对象来对待。当我们说函数是第一类对象的时候,就是说函数也能够实现以下功能。

  • 通过字面量创建。

1function ninjaFunction() {}

  • 赋值给变量,数组项或其他对象的属性。

1var ninjaFunction = function() {};  ?--- 为变量赋值一个新函数
2ninjaArray.push(function(){});   ?--- 向数组中增加一个新函数
3ninja.data = function(){};  ?--- 给某个对象的属性赋值为一个新函数

  • 作为函数的参数来传递。

1function call(ninjaFunction){
2 ninjaFunction();
3}
4call(function(){});   ?--- 一个新函数作为参数传递给函数

  • 作为函数的返回值。

1function returnNewNinjaFunction() {
2 return function(){};   ?--- 返回一个新函数
3}

  • 具有动态创建和分配的属性。

1var ninjaFunction = function(){};
2ninjaFunction.ninja = "Hanzo";  ?--- 为函数增加一个新属性

对象能做的任何一件事,函数也都能做。函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作。

{JavaScript中的函数式编程!}

  • 把函数作为第一类对象是函数式编程(functional programming)的第一步,函数式编程是一种编程风格,它通过书写函数式(而不是指定一系列执行步骤,就像那种更主流的命令式编程)代码来解决问题。函数式编程可以让代码更容易测试、扩展及模块化。不过这是一个很大的话题,因此本文仅对这个特性做了肯定。如果你对如何在JavacScript中利用函数式编程感兴趣,推荐阅读Luis Atencio著(由Manning出版社2016年出版)的《JavaScript函数式编程》,购买方式见www.manning.com/ books/functional-programming- in-JavaScript。

第一类对象的特点之一是,它能够作为参数传入函数。对于函数而言,这项特性也表明:如果我们将某个函数作为参数传入另一个函数,传入函数会在应用程序执行的未来某个时间点才执行。大家所知道的更一般的概念是回调函数(callback function)。下面我们来学习这个重要概念。

你知道吗?

关注微信公众号【异步图书】每周送书

1.2.1 存储函数

某些例子中(例如,我们需要管理某个事件发生后需要调用的回调函数集合),我们会存储元素唯一的函数集合。当我们向这样的集合中添加函数时,会面临两个问题:哪个函数对于这个集合来说是一个新函数,从而需要被加入到该集合中?又是哪个函数已经存在于集合中,从而不需要再次加入到集合中?一般来说,管理回调函数集合时,我们并不希望存在重复函数,否则一个事件会导致同一个回调函数被多次调用。

一种显著有效的简单方法是把所有函数存入一个数组,通过循环该数组来检查重复函数。令人遗憾的是,这种方法的性能较差,尤其作为一个“忍者”要把事情干得漂亮而不仅是做到能用。我们可以使用函数的属性,用适当的复杂度来实现它,如清单1.2所示。

清单1.2 存储唯一函数集合

1var store = {
2 nextId: 1,  ?--- 跟踪下一个要被复制的函数
3 cache: {},  ?--- 使用一个对象作为缓存,我们可以在其中存储函数
4 add: function(fn) { 
5   if (!fn.id) {
6    fn.id = this.nextId ;
7    this.cache[fn.id] = fn;
8    return true;
9  }
10 }  ?--- 仅当函数唯一时,将该函数加入缓存
11};
12function ninja(){}
13assert(store.add(ninja), 
14     "Function was safely added.");
15assert(!store.add(ninja),
16     "But it was only added once.");   ?--- 测试上面代码按预期工作

在这个清单中,我们创建了一个对象赋值给变量store,这个变量中存储的是唯一的函数集合。这个对象有两个数据属性:其一是下一个可用的id,另外一个缓存着已经保存的函数。函数通过add()方法添加到缓存中。

1add: function(fn) {
2  if (!fn.id) {
3   fn.id = this.nextId ;
4   this.cache[fn.id] = fn;
5   return true;
6  }
7...

在add函数内,我们首先检查该函数是否已经存在id属性。如果当前的函数已经有id属性,我们则假设该函数已经被处理过了,从而忽略该函数,否则为该函数分配一个id(同时增加nextId)属性,并将该函数作为一个属性增加到cache上,id作为属性名。紧接着该函数的返回值为true,从而可得知调用了add()后,函数是什么时候被添加到存储中的。

在浏览器中运行该程序后,页面显示:测试程序尝试两次添加ninja()函数,而该函数只被添加一次到存储中,如图1.3所示。第9章展示了用于操作合集的更好技术,它利用了ES6的新的对象类型集合(Set)。

分分快三计划 5

image

图1.3 给函数附加一个属性后,我们就能够引用该属性。本例通过这种方式可以确保该ninja函数仅被添加到函数中一次

另外一种有用的技巧是当使用函数属性时,可以通过该属性修改函数自身。这个技术可以用于记忆前一个计算得到的值,为之后计算节省时间。

函数声明

JavaScript定义函数的最基本方式是函数声明(见图1.4)。正如你所见,每个函数声明以强制性的function开头,其后紧接着强制性的函数名,以及括号和括号内一列以逗号分隔的可选参数名。函数体是一列可以为空的表达式,这些表达式必须包含在花括号内。除了这种形式以外,每个函数声明还必须包含一个条件:作为一个单独的JavaScript语句,函数声明必须独立(但也能够被包含在其他函数或代码块中,在下一小节中你将会准确理解其含义)。

分分快三计划 6

image

图1.4 函数声明是独立的,是独立的JavaScript代码块(它可以被包含在其他函数中)

清单1.4展示了两条函数声明例子。

清单1.4 函数声明示例

1function samurai() {
2 return "samurai here";  ?--- 在全局代码中定义samurai函数
3}
4function ninja() {  ?--- 在全局代码中定义ninja函数
5 function hiddenNinja() {
6  return "ninja here";
7 }  ?--- 在ninja函数内定义hiddenNinja函数
8 return hiddenNinja();
9}

如果你对函数式语言没有太多了解,仔细看一看,你可能会发现你并不习惯这种使用方式: 一个函数被定义在另一个函数之中!

1function ninja() {
2 function hiddenNinja() {
3   return "ninja here";
4 }
5 return hiddenNinja();
6}

在JavaScript中,这是一种非常通用的使用方式,这里用它作为例子是为了再次强调JavaScript中函数的重要性。

{注意 }

让函数包含在另一个函数中可能会因为忽略作用域的标识符解析而引发一些有趣的问题,但现在可以先留下这个问题,第5章会重新回顾这个问题的细节。

1.3 函数定义

JavaScript函数通常由函数字面量(function literal)来创建函数值,就像数字字面量创建一个数字值一样。要记住这一点,作为第一类对象,函数是可以用在编程语言中的值,就像例句字符串或数字的值。无论你是否意识到了这一点,你一直都是这样做的。

JavaScript提供了几种定义函数的方式,可以分为4类。

  • 函数定义(function declarations)和函数表达式(function expressions)——最常用,在定义函数上却有微妙不同的的两种方式。人们通常不会独立地看待它们,但正如你将看到的,意识到两者的不同能帮我们理解函数何时能够被调用。

    1function myFun(){ return 1;}

  • 箭头函数(通常被叫做lambda函数)——ES6新增的JavaScript标准,能让我们以尽量简洁的语法定义函数。

    1myArg => myArg*2

  • 函数构造函数—— 一种不常使用的函数定义方式,能让我们以字符串形式动态构造一个函数,这样得到的函数是动态生成的。这个例子动态地创建了一个函数,其参数为a和b,返回值为两个数的和。

    1new Function('a', 'b', 'return a b')

  • 生成器函数——ES6新增功能,能让我们创建不同于普通函数的函数,在应用程序执行过程中,这种函数能够退出再重新进入,在这些再进入之间保留函数内变量的值。我们可以定义生成器版本的函数声明、函数表达式、函数构造函数。

    1function* myGen(){ yield 1; }

理解这几种方式的不同很重要,因为函数创建的方式很大程度地影响了函数可被调用的时间、函数的行为以及函数可以在哪个对象上被调用。

这一节中,我们将会探索函数定义、函数表达式和箭头函数。你将学到它们的语法和它们的工作方式,我们也将会在本文中多次回顾它们的细节。另一方面,生成器函数则有一点独特,它不同于普通函数。在第6章我们会再来学习它们的细节。

剩下的JavaScript特性——函数构造函数我们将全部跳过。尽管它具有某些有趣的应用场景,尤其是在动态创建和执行代码时,但我们依然认为它是JavaScript语言的边缘功能。如果你想知道更多关于函数构造函数的信息,请访问http://mng.bz/ZN8e。

让我们先用最简单、最传统的方式定义函数吧:函数声明和函数表达式。

  • 回调函数在哪种情况下会同步调用,或者异步调用呢?

  • 箭头函数和函数表达式的区别是什么?

  • 你为什么需要在函数中使用默认参数?

1.3.2 箭头函数

注意:

箭头函数是JavaScript标准中的ES6新增项(浏览器兼容性可参考http://mng.bz/8bnH)。

由于JavaScript中会使用大量函数,增加简化创建函数方式的语法十分有意义,也能让我们的开发者生活更愉快。在很多方式中,箭头函数是函数表达式的简化版。一起来回顾一下本文开始的排序例子。

1var values = [0, 3, 2, 5, 7, 4, 8, 1];
2values.sort(function(value1,value2){
3 return value1 – value2;
4});

这个例子中,数组对象的排序方法的参数传入了一个回调函数表达式,JavaScript引擎会调用这个回调函数以降序排序数组。现在来看看如何用箭头函数来做完全相同的工作:

1var values = [0, 3, 2, 5, 7, 4, 8, 1];
2
3values.sort((
4 5value1,value2) => value1 – value2
6
7);

看到这是多么简洁了吧?

这种写法不会产生任何因为书写function关键字、大括号或者return语句导致的混乱。箭头函数语句有着比函数表达式更为简单的方式:函数传入两个参数并返回其差值。注意这个新操作符——胖箭头符号=>(等号后面跟着大于号)是定义箭头函数的核心。

现在来解析箭头函数的语法,首先看看它的最简形式:

1param => expression

这个箭头函数接收一个参数并返回表达式的值,如下面的清单1.6就使用了这种语法。

清单1.6 比较箭头函数和函数表达式

1var greet = name => "Greetings " name;  ?--- 定义箭头函数
2
3assert(greet("Oishi") === "Greetings Oishi", "Oishi is properly greeted")
4;
5
6var anotherGreet = function(nam
7e){
8 return "Greetings " n
9ame;
10};  ?--- 定义
11函数表达式
12assert(anotherGreet("Oishi") === "Greetings O
13ishi",
14     "Again, Oishi is properly greeted");

稍作欣赏,使用箭头函数的代码即简洁又清楚。这是箭头函数的最简语法,但一般情况下,箭头函数会被定义成两种方式,如图1.6所示。

稍作欣赏,使用箭头函数的代码即简洁又清楚。这是箭头函数的最简语法,但一般情况下,箭头函数会被定义成两种方式,如图1.6所示。

分分快三计划 7

image

图1.6 箭头函数的语法

如你所见,箭头函数的定义以一串可选参数名列表开头,参数名以逗号分隔。如果没有参数或者多余一个参数时,参数列表就必须包裹在括号内。但如果只有一个参数时,括号就不是必须的。参数列表之后必须跟着一个胖箭头符号,以此向我们和JavaScript引擎指示当前处理的是箭头函数。

胖箭头操作符后面有两种可选方式。如果要创建一个简单函数,那么可以把表达式放在这里(可以是数学运算、其他的函数调用等),则该函数的返回值即为此表达式的返回值。例如,第一个箭头函数的示例如下:

1var greet = name => "Greetings " name;

这个箭头函数的返回值是字符串“Greetings”和参数name的结合。在其他案例中,当箭头函数没那么简单从而需要更多代码时,箭头操作符后则可以跟一个代码块,例如:

1var greet = name => {
2 var helloString = 'Greetings ';
3 return helloString name;
4};

这段代码中箭头函数的返回值和普通函数一样。如果没有return语句,返回值是undefined;反之,返回值就是return表达式的值。

在本文中我们会多次回顾箭头函数。除此之外,我们还会展示箭头函数的一些额外功能,它能帮助我们规避一些在很多标准函数中可能遇到的难以捉摸的缺陷。箭头函数和很多其他函数一样,可以通过接收参数来执行任务。接下来看看当向函数内传入参数后,该参数值发生了什么。

函数表达式

正如我们多次所提到的,JavaScript中的函数是第一类对象,除此以外也就意味着它们可以通过字面量创建,可以赋值给变量和属性,可以作为传递给其他函数的参数或函数的返回值。正因为函数有如此的基础结构,所以JavaScript能让我们把函数和其他表达式同等看待。例如,如下例子中我们可以使用数字字面量:

1var a = 3;
2myFunction(4);

同样,在相同位置可以用函数字面量:

1var a = function() {};
2myFunction(function(){});

这种总是其他表达式的一部分的函数(作为赋值表达式的右值,或者作为其他函数的参数)叫作函数表达式。函数表达式非常重要,在于它能准确地在我们需要使用的地方定义函数,这个过程能让代码易于理解。清单1.5展示了函数声明和函数表达式的不同之处。

清单1.5 函数声明和函数表达式

1<pre class="代码无行号"><code>function myFunctionDeclaration(){  ?--- 独立的函数声明
2 function innerFunction() {}  ?--- 内部函数声明
3}
4var myFunc = function(){};  ?--- 函数表达式作为变量声明赋值语句中的一部分
5myFunc(function(){  ?--- 函数表达式作为一次函数调用中的参数
6  return function(){};  ?--- 函数表达式作为函数返回值
7});
8(function <strong>namedFunctionExpression</strong> () {
9})();  ?--- 作为函数调用的一部分,命名函数表达式会被立即调用
10 function(){}();
11-function(){}();
12!function(){}();
13~function(){}();  ?--- 函数有达式可以作为一元操作符的参数立即调用</code></pre>

示例代码的开头是标准函数声明,其包含一个内部函数声明:

1function myFunctionDeclaration(){
2 function innerFunction() {}
3}

从这个示例中你能够看到,函数声明是如何作为JavaScript代码中的独立表达式的,但它也能够包含在其他函数体内。与之比较的是函数表达式,它通常作为其他语句的一部分。它们被放在表达式级别,作为变量声明(或者赋值)的右值:

1var myFunc = function(){};

或者作为另一个函数调用的参数或返回值。

1myFunc(function() {
2 return function(){};
3});

函数声明和函数表达式除了在代码中的位置不同以外,还有一个更重要的不同点是:对于函数声明来说,函数名是强制性的,而对于函数表达式来说,函数名则完全是可选的。

函数声明必须具有函数名是因为它们是独立语句。一个函数的基本要求是它应该能够被调用,所以它必须具有一种被引用方式,于是唯一的方式就是通过它的名字。

从另一方面来看,函数表达式也是其他JavaScript表达式的一部分,所以我们也就具有了调用它们的替代方案。例如,如果一个函数表达式被赋值给了一个变量,我们可以用该变量来调用函数。

1var doNothing = function(){};
2doNothing();

或者,如果它是另外一个函数的参数,我们可以在该函数中通过相应的参数名来调用它。

1function doSomething(action) {
2 action();
3}

立即函数

函数表达式可以放在初看起来有些奇怪的位置上,例如通常认为是函数标识符的位置。接下来仔细看看这个构造(如图1.5所示)。

分分快三计划 8

image

图1.5 标准函数的调用和函数表达式的立即调用的对比

当想进行函数调用时,我们需要使用能够求值得到函数的表达式,其后跟着一对函数调用括号,括号内包含参数。在最基本的函数调用中,我们把求值得到函数的标识符作为左值(如图1.5所示)。不过用于被括号调用的表达式不必只是一个简单的标识符,它可以是任何能够求值得到函数的表达式。例如,指定一个求值得到函数的表达式的最简单方式是使用函数表达式。如图1.5中右图所示,我们首先创建了一个函数,然后立即调用这个新创建的函数。这种函数叫作立即调用函数表达式(IIFE),或者简写为立即函数。这一特性能够模拟JavaScript中的模块化,故可以说它是JavaScript开发中的重要理念。第11章中会集中讨论IIFE的应用。

{加括号的函数表达式!}

  • 还有一件可能困扰你的是上面例子中我们立即调用的函数表达式方式:函数表达式被包裹在一对括号内。为什么这样做呢?其原因是纯语法层面的。JavaScript解析器必须能够轻易区分函数声明和函数表达式之间的区别。如果去掉包裹函数表达式的括号,把立即调用作为一个独立语句function() {}(3),JavaScript开始解析时便会结束,因为这个独立语句以function开头,那么解析器就会认为它在处理一个函数声明。每个函数声明必须有一个名字(然而这里并没有指定名字),所以程序执行到这里会报错。为了避免错误,函数表达式要放在括号内,为JavaScript解析器指明它正在处理一个函数表达式而不是语句。
  • 还有一种相对简单的替代方案(function(){}(3))也能达到相同目标(然而这种方案有些奇怪,故不常使用)。把立即函数的定义和调用都放在括号内,同样可以为JavaScript解析器指明它正在处理函数表达式。

表1.5中最后4个表达式都是立即调用函数表达式主题的4个不同版本,在JavaScript库中会经常见到这几种形式:

1 function(){}();
2-function(){}();
3!function(){}();
4~function(){}();

不同于用加括号的方式区分函数表达式和函数声明,这里我们使用一元操作符 、-、!和~。这种做法也是用于向JavaScript引擎指明它处理的是表达式,而不是语句。从计算机的角度来讲,注意应用一元操作符得到的结果没有存储到任何地方并不重要,只有调用IIFE才重要。现在我们已经学会了JavaScript中两种基本的函数定义方式(函数声明和函数表达式)的细节。接下来开始探索JavaScript标准中的新增特性:箭头函数。

本文由分分快三计划发布,转载请注明来源

关键词: 分分快三计划 JavaScript 移动开发