『翻译』JavaScript 函数式编程

Read the originaljavascript


前言

函数式编程与咱们以往的编程习惯有许多不一样。这篇文章举了一些JavaScript的例子,介绍了函数式编程中重要的概念。附加的文章会让你更深刻的了解JavaScript中的函数式编程。html

本文源码能够在GitHub上找到,放在jsFunctionalProgramming仓库中。java

我要感谢Csaba Hellinger的支持和投入,在他的帮助下我才完成这篇文章。git

PART 1

函数式编程由Lambda Calculus演化而来,它是一个抽象数学的函数表述,咱们将思考怎么把它运用在现实中。es6

函数式编程是声明式编程的范式。github

为何要使用函数式编程?

函数式编程有如下具体特性:算法

  1. 避免状态改变(可变的数据) - 函数式编程的特性之一就是:函数在应用中不会改变状态,它们(functions)宁愿从以前的状态之上建立一个新的状态。编程

  2. 函数声明 vs 函数表达式 - 在函数式编程中,咱们定义和描述函数就像数学中的一个方法声明。数组

  3. 幂等性 - 这意味着咱们用相同的参数调用一个函数(无论任什么时候刻)它都会返回相同的结果,这也能够避免状态的改变。浏览器

这三个特性咋一看彷佛并无什么意义,但若是咱们更深刻的分析,发如今如下三种状况下使用函数式编程能充分发挥这三个特性:

  1. 并行的代码执行 - 由于函数式编程有幂等性避免状态改变的特性,用函数方法编写代码会让并行更容易,由于不会出现同步问题。

  2. 简明、简洁的代码 - 由于函数式编程使用方法声明的方式,代码不会像面向过程编程同样,有额外的算法步骤。

  3. 不一样的编程思想 - 一旦你真正使用了一门函数式编程语言,你会拥有一种新的编程思想,当你构建应用时也会有新的点子。

f(x) === J(s)

javascript 是一门真正的(纯粹的)函数式编程语言吗?

不!JavaScript并非一门纯粹的函数式编程语言...

第一型对象 - 函数

它能够很好的运用在函数式编程中,由于函数是第一性对象。若是在一门编程语言中,函数和其余类型同样,那么这门语言中的函数就是第一型对象。举个例子,函数能够做为参数传递给其余函数,也能够赋值给变量。

咱们将检查一些函数是不是第一型对象,可是在这以前,咱们先构建一个代码块,咱们将像真正的函数式语言同样使用JavaScript。

在大部分纯函数式编程语言中(Haskell, Clean, Erlang),它们是没有for或者while循环的,因此循环一个列表须要用到递归函数。纯函数式编程语言有语言支持和最好的列表推导式和列表串联。

这里有一个函数实现了for循环,咱们将在接下来的代码中用到它,可是你也将看到它在JS中的局限性,由于尾部调用优化并无被普遍的支持,但之后会好起来的。

function funcFor(first, last, step, callback) {

  //
  //递归inner函数
  //
  function inner(index) {
    if((step > 0 && index >= last) || (step < 0 && index < last)) {
      return;
    }

    callback(index);

    //
    //接下来进行尾部调用
    //
    inner(index + step);
  }

  //
  //开始递归
  //
  inner(first);
}复制代码

inner函数包含了对中止递归的管理,它传入参数index去调用callback,再递归调用inner(index + step)确保循环传递到下一步。

递归是函数式编程的一个重要方面。

如今,让咱们看看真正的函数式编程:

function applyIfAllNumbers(items, fn) {
  if(areNumbers(items)) {
    return funcMap(items, fn);
  }
  return [];
}复制代码

applyIfAllNumbers函数的目的是调用fn函数,并把items中的每一个数字做为参数传入,但前提是只有在items数组中都是数字的状况下才去调用。

下面是验证器函数:

function areNumbers(numbers) {
  if(numbers.length == 0) {
    return true;
  }
  else {
    return isNumber(number[0]) && areNumbers(numbers.slice(1));
  }
}

function isNumber(n) {
  return isFinite(n) && +n === n;
}复制代码

这段代码简单明了,若是参数是一个数字,isNumber函数返回true,不然返回falseareNumbers函数使用isNumber函数判断numbers数组中是否全是数字(再提醒一次,递归经常被用来实现这种逻辑)。

另外一个例子是applyForNumbersOnly

function applyForNumbersOnly(items, fn) {
  let numbers = filter(items, isNumber);
  return funcMap(numbers, fn);
}复制代码

这样写甚至更简洁:

function applyForNumbersOnly(items, fn) {
  return funcMap(filter(items, isNumber), fn);
}复制代码

applyForNumbersOnly调用fn方法仅仅是为了收集items中的数字。

funcMap函数在函数式编程中重现了著名的map函数,可是这里我借助了funcForEach函数来建立它:

function funcForEach(items, fn) {
  return funcFor(0, items.length, 1, function(idx) {
    fn(items[idx]);
  });
}

function funcMap(items, fn) {
  let result = [];
  funcForEach(items, function(item) {
    result.push(fn(item));
  });
  return result;
}复制代码

最后还剩filter函数,咱们再一次使用递归来实现过滤的逻辑。

function filter(input, callback) {
  function inner(input, callback, index, output) {
    if (index === input.length) {
      return output;
    }
    return inner(
      input,
      callback,
      index + 1,
      callback(input[index]) ? output.concat(input[index]) : output;
    );
  }
  return inner(input, callback, 0, []);
}复制代码

JS中的尾调用优化(TCO)

EcmaScript 2015 TCO文档中有一些用例的定义,这门语言不久就将支持尾调用优化了。最关键的一点就是在你的代码中使用use strict模式,不然JS不能支持尾调用优化。

因为没有内置方法来检测浏览器是否支持尾调动优化,如下代码实现了这个功能:

"use static"

function isTCOSupported() {
  const outerStackLen = new Error().stack.length;
  //inner函数的name长度必定不能超过外部函数
  return (function inner() {
    const innerStackLen = new Error().stack.length;
    return innerStackLen <= outerStackLen;
  }());
}

console.log(isTCOSupported() ? "TCO Available" : "TCO N/A");复制代码

这里有一个重现Math.pow函数的例子,它能从EcmaScript 2015的TCO中获益。

这个pow函数的实现使用了ES6默认参数,让它看上去更简洁。

function powES6(base, power, result=base) {
  if (power === 0) {
    return 1;
  }

  if(power === 1) {
    return result;
  }

  return powES6(base, power - 1, result * base);
}复制代码

首先要提醒如下,powES6函数有三个参数而不是两个。第三个参数是计算后的值。咱们随身携带return是为了实现让咱们的递归调用变成真正的尾调用,让JS可使用它的尾调用优化技术。

万一咱们不能使用ES6的特性,那么咱们不推荐使用递归去实现pow函数,由于这门语言尚未提出有关递归的优化,这样实现起来就很复杂了:

function recursivePow(base, power, result) {  
    if (power === 0) {
        return 1;
    }
    else if(power === 1) {
        return result;
    }

    return recursivePow(base, power - 1, result * base);
}

function pow(base, power) {  
    return recursivePow(base, power, base);
}复制代码

咱们把递归计算放在了另外一个recursivePow函数中,这个函数有三个参数,就像powES6函数同样。使用一个新函数并把base做为参数传递给它,以此实现ES6中的默认参数逻辑。

这个页面你能够查看TCO在不一样浏览器和平台的支持状况。

目前只有Safari 10是彻底支持TCO的浏览器(在写这篇文章时),我将进行一些对于pow的测试,来看看它的表现。

测试递归调用

我使用了powES6pow函数来进行测试:

"use strict";

function stressPow(n) {  
    var result = [];
    for (var i=0; i<n; ++i) {
        result.push(
          pow(2, 0),
          pow(2, 1),
          pow(2, 2),
          pow(2, 3),
          pow(2, 4),
          pow(2, 5),
          pow(2, 10),
          pow(2, 20),
          pow(2, 30),
          pow(1, 10000),
          pow(2, 40),
          pow(3, 10),
          pow(4, 15),
          pow(1, 11000),
          pow(3.22, 125),
          pow(3.1415, 89),
          pow(7, 2500),
          pow(2, 13000)
        );
    }

    return result;
}

var start = performance.now();
var result_standard = stressPow(2500);  
var duration = performance.now() - start;  
console.log(result_standard);  
console.log(`Duration: ${duration} ms.`);复制代码

我在Chrome v55, Firefox v50, Safari v9.2 和 Safari v10上测试了以上代码。

小结

根据上面的数据,咱们得出Safari对递归函数的优化效率是最高的。Safari 10对尾调用的支持是最好的,速度比Chrome快了大约2.8倍。Firefox几乎和Safari 9.2 同样棒,这出乎了个人意料。

若是你很喜欢这篇文章,请点个赞哦。(译者注:话说好长啊,好累啊。)

让咱们继续函数式!

PART 2 也即将发出,关于高阶函数和例子,讲解如何编写函数式风格的代码。


喜欢本文的朋友能够关注个人微信公众号,不按期推送一些好文。

本文由Rockjins Blog翻译,转载请与译者联系。不然将追究法律责任。

相关文章
相关标签/搜索