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
方法是处理数组更通用的方式,而且filter
和map
方法都可以当作是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
的返回值决定数组元素的排序方式: 如果两个元素 a
和 b
,compareFunction(a,b)
返回一个比 0 小的值,那么 a
会在 b
的前面。 如果两个元素 a
和 b
,compareFunction(a,b)
返回一个比 0 大的值,那么 b
会在 a
的前面。 如果两个元素 a
和 b
,compareFunction(a,b)
返回等于 0 的值,那么 a
和 b
的位置保持不变。
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