『翻譯』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翻譯,轉載請與譯者聯繫。不然將追究法律責任。

相關文章
相關標籤/搜索