JavaScript 函数式编程

函数式编程 Functional Programming

什么是函数式编程

函数式编程是一种方案简单、功能独立、对作用域外没有任何副作用的编程范式(paradigms):INPUT -> PROCESS -> OUTPUT

函数式编程:

1)功能独立——不依赖于程序的状态(比如可能发生变化的全局变量);

2)纯函数——同一个输入永远能得到同一个输出;

3)有限的副作用——可以严格地限制函数外部对状态的更改。

函数式编程中使用较多的变量和函数,并且只有需要计算时才把数据传给函数,其他情况下函数和数据是分离的。函数会返回新数值并传递到代码的其他部分。而在面向对象编程中,变量和函数被写在一个对象内部。

一些术语 Terminology

  • 回调 Callbacks:是被传递到另一个函数中调用的函数。 例如在 filter 中,回调函数告诉 JavaScript 以什么规则过滤数组。
  • 第一等公民 First-class citizens:函数就像数值一样,可以赋值给变量、传递给另一个函数,或从其它函数返回,这种函数叫做头等 first class 函数。 在 JavaScript 中,所有函数都是头等函数。
  • 高阶函数 Higher order:将其他函数作为参数或/和被调用时返回函数的函数。当函数被传递给另一个函数或从另一个函数返回时,那些传入或返回的函数可以叫做 lambda。
  • 纯函数和副作用 Pure functions and side effects:纯函数指同一输入得到同一输出,传函数没有副作用。副作用指函数使其外部发生改变的后果,包括:
    • 改变函数外的变量值或者依赖函数外的变量
    • 调用浏览器 API,甚至是控制台
    • 调用类似 Math.random() 这样的方法 - 因为这个值不能可靠地重复
const prepareGreenTea = () => 'greenTea';

const prepareBlackTea = () => 'blackTea';

const getTea = (prepareTea, numOfCups) => {
  const teaCups = [];

  for(let cups = 1; cups <= numOfCups; cups += 1) {
    const teaCup = prepareTea();
    teaCups.push(teaCup);
  }
  return teaCups;
};

const tea4GreenTeamFCC = getTea(prepareGreenTea, 27);
const tea4BlackTeamFCC = getTea(prepareBlackTea, 13);

console.log(
  tea4GreenTeamFCC,
  tea4BlackTeamFCC
);

副作用和避免变化 Avoid Mutation and Side Effect

函数式编程的核心原则之一是不改变任何东西。 变化会导致错误。 如果一个函数不改变传入的参数、全局变量等数据,那么它造成问题的可能性就会小很多。

在函数式编程中,改变或变更叫做 mutation,这种改变的结果叫做“副作用”(side effect)。 理想情况下,函数应该是不会产生任何副作用的 pure function (纯函数)。

let fixedValue = 4;

function incrementer() {
    return fixedValue + 1;
} // 不能写 fixedValue++;

传参以避免外部依赖

函数式编程的另一个原则是:总是显式声明依赖关系。 如果函数依赖于一个变量或对象,那么将该变量或对象作为参数直接传递到函数中。

这样做会有很多好处。 其中一点是让函数更容易测试,因为知道参数是什么,并且这个参数也不依赖于程序中的任何其他内容。

其次,这样做可以更加自信地更改,删除或添加新代码。 因为很清楚哪些是可以改的,哪些是不可以改的,这样就知道哪里可能会有潜在的陷阱。

最后,无论代码的哪一部分执行它,函数总是会为同一组输入生成相同的输出。

let fixedValue = 4;

function incrementer(num) {
  return num + 1;
}

重构全局变量 Refactor

重构代码,使全局数组 bookList 在函数内部不会被改变。 add 函数可以将指定的 bookName 增加到数组末尾并返回一个新的数组(列表)。 remove 函数可以从数组中移除指定 bookName

// 全局变量
const bookList = ["The Hound of the Baskervilles", "On The Electrodynamics of Moving Bodies", "Philosophiæ Naturalis Principia Mathematica", "Disquisitiones Arithmeticae"];

// 修改这行下面的代码
function add (book,bookName) {
  let newBook = [...book];
  newBook.push(bookName);
  return newBook;

  // 修改这行上面的代码
}

// 修改这行下面的代码
function remove (book,bookName) {
  let newBook = [...book];
  const book_index = newBook.indexOf(bookName);
  if (book_index >= 0) {

    newBook.splice(book_index, 1);
    return newBook;

    // 修改这行上面的代码
    }
}

const newBookList = add(bookList, 'A Brief History of Time');
const newerBookList = remove(bookList, 'On The Electrodynamics of Moving Bodies');
const newestBookList = remove(add(bookList, 'A Brief History of Time'), 'On The Electrodynamics of Moving Bodies');

console.log(bookList);

map 迭代

函数在 JavaScript 中被视为 First Class Objects,它们可以像任何其他对象一样使用。 它们可以保存在变量中,存储在对象中,也可以作为函数参数传递。

数组的 map 方法(即 Array.prototype.map())。请记住,map方法是迭代数组中每一项的方式之一。 在对每个元素应用回调函数后,它会创建一个新数组(不改变原来的数组)。 它这样做时没有改变原始数组。

当调用回调函数时,传入了三个参数。 第一个参数是当前正在处理的数组项。 第二个参数是当前数组项的索引值,第三个参数是在其上调用 map 方法的数组。

看下在 users 上使用 map 方法的例子,返回了一个新数组只包含了用户的名字。 为了简化,例子里只使用了回调函数的第一个参数。

const users = [
  { name: 'John', age: 34 },
  { name: 'Amy', age: 20 },
  { name: 'camperCat', age: 10 }
];

const names = users.map(user => user.name);
console.log(names);
// [ 'John', 'Amy', 'camperCat' ]

实现 map

const s = [23, 65, 98, 5];

Array.prototype.myMap = function(callback) {

var newArray = [];

for (let i = 0; i < this.length; i++) {
    newArray.push(callback(this[i]));
  }
  return newArray;
};

const new_s = s.myMap(function(item) {
  return item * 2;
});

filter 从数组提取数据

另一个有用的数组方法是 filter()(即 Array.prototype.filter())。

filter 接收一个回调函数,将回调函数内的逻辑应用于数组的每个元素,新数组包含根据回调函数内条件返回 true 的元素。 换言之,它根据传递给它的函数过滤数组。 和 map 一样,filter 不会改变原始数组。

回调函数接收三个参数。 第一个参数是当前正在被处理的元素。 第二个参数是这个元素的索引,第三个参数是在其上调用 filter 方法的数组。

看下在 users 上使用 filter 方法的例子,返回了一个包含了 30 岁以下的用户新数组。 为了简化,例子里只使用了回调函数的第一个参数。

const users = [
  { name: 'John', age: 34 },
  { name: 'Amy', age: 20 },
  { name: 'camperCat', age: 10 }
];

const usersUnder30 = users.filter(user => user.age < 30);
console.log(usersUnder30);  
//  [ { name: 'Amy', age: 20 }, { name: 'camperCat', age: 10 } ]

实现 filter

const s = [23, 65, 98, 5];

Array.prototype.myFilter = function(callback) {
   let newArray = [];
  this.forEach(function(x) {
    if (callback(x) == true) {
      newArray.push(x);
    }
  });
  return newArray;
};

const new_s = s.myFilter(function(item) {
  return item % 2 === 1;
});

concat 拼接

Concatenation 意思是将元素连接到尾部。 同理,JavaScript 为字符串和数组提供了concat方法。 对数组来说,在一个数组上调用 concat 方法,然后提供另一个数组作为参数添加到第一个数组末尾。 它返回一个新数组,不会改变任何一个原始数组。

function nonMutatingConcat(original, attach) {
  return original.concat(attach);
}

reduce 分析数据

reduce()(即Array.prototype.reduce()),是 JavaScript 所有数组操作中最常用的方法。 几乎可以用reduce方法解决所有数组处理问题。

reduce方法是处理数组更通用的方式,而且filtermap方法都可以当作是reduce的特殊实现。 reduce方法遍历数组中的每个项目并返回单个值(即字符串、数字、对象、数组)。 这是通过在每次迭代中调用一个回调函数来实现的。

回调函数接受四个参数。 第一个参数称为叠加器,它是上一次迭代中回调函数的返回值,第二个参数是当前正在处理的数组元素,第三个参数是该参数的索引,第四个参数是在其上调用 reduce 方法的数组。

除了回调函数,reduce 还有一个额外的参数做为叠加器的初始值。 如果没有第二个参数,会跳过第一次迭代,第二次迭代给叠加器传入数组的第一个元素。

见下面的例子,给 users 数组使用 reduce 方法,返回所有用户数组的和。 为了简化,例子仅使用了回调函数的第一个参数和第二个参数。

const users = [
  { name: 'John', age: 34 },
  { name: 'Amy', age: 20 },
  { name: 'camperCat', age: 10 }
];

const sumOfAges = users.reduce((sum, user) => sum + user.age, 0);
console.log(sumOfAges); // 64

sort 排序

sort 方法可以根据回调函数对数组元素进行排序。

JavaScript 的默认排序方法是 Unicode 值顺序排序,有时可能会得到意想不到的结果。 因此,建议提供一个回调函数来指定如何对数组项目排序。 这个回调函数通常叫做 compareFunction,它根据 compareFunction 的返回值决定数组元素的排序方式: 如果两个元素 abcompareFunction(a,b) 返回一个比 0 小的值,那么 a 会在 b 的前面。 如果两个元素 abcompareFunction(a,b) 返回一个比 0 大的值,那么 b 会在 a 的前面。 如果两个元素 abcompareFunction(a,b) 返回等于 0 的值,那么 ab 的位置保持不变。

function alphabeticalOrder(arr) {return arr.sort(function (a, b) {
    return a === b ? 0 : a < b ? -1 : 1;
  })
}

alphabeticalOrder(["a", "d", "c", "a", "z", "g"]);

split 拆分字符串成数组

split 方法将一个字符串分割成一个字符串数组。 它需要一个参数作为分隔符,它可以是用于拆分字符串或正则表达式的一个字符。 举个例子,如果分隔符是空格,得到一个单词数组;如果分隔符是空字符串,得到一个由字符串中每个字符组成的数组。

function splitify(str) {
  return str.split(/\W/);
}
splitify("Hello World,I-am code");

join 数组组合成字符串

join 方法用来把数组中的所有元素放入一个字符串。 并通过指定的分隔符参数进行分隔。

function sentensify(str) {
  return str.split(/\W/).join(" ");
}
sentensify("May-the-force-be-with-you");

every 全部元素

every 方法用于检测数组中所有 元素是否都符合指定条件。 如果所有元素满足条件,返回布尔值 true,反之返回 false

function checkPositive(arr){
  return arr.every(item => item > 0);
}

some 部分元素

some 方法用于检测数组中任何 元素是否满足指定条件。 如果有一个元素满足条件,返回布尔值 true,反之返回 false

function checkPositive(arr) {
  return arr.some(item => item > 0);
}

函数柯里化和局部调用 Currying and Partial Application

arity(参数个数)是函数所需的形参的数量。 函数柯里化(Currying)意思是把接受多个 arity 的函数变换成接受单一 arity 的函数。

换句话说,就是重构函数让它接收一个参数,然后返回接收下一个参数的函数举个例子:

function unCurried(x, y) {
  return x + y;
}

function curried(x) {
  return function(y) {
    return x + y;
  }
}

const curried = x => y => x + y

curried(1)(2) // 3

柯里化在不能一次为函数提供所有参数情况下很有用。 因为它可以将每个函数的调用保存到一个变量中,该变量将保存返回的函数引用,该引用在下一个参数可用时接受该参数。 下面是使用柯里化函数的例子:

const funcForY = curried(1);
console.log(funcForY(2)); // 3

类似地,局部调用( partial application)的意思是一次对一个函数应用几个参数,然后返回另一个应用更多参数的函数。 这是一个示例:

function impartial(x, y, z) {
  return x + y + z;
}

const partialFn = impartial.bind(this, 1, 2);
partialFn(10); // 13
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容