函数式编程
函数式编程是一种基于函数计算的软件开发方法。像数学一样,函数在编程中通过输入产生输出。函数式编程遵循几个核心原则:
- 独立于程序状态或全局变量,只依赖于传递给它们的参数进行计算
- 限制更改程序状态,避免更改保存数据的全局对象
- 对程序的副作用尽量小
学习函数式编程
函数式编程是一种解决方案简单,功能独立,对作用域外没有任何副作用的编程范式。INPUT -> PROCESS -> OUTPUT
函数式编程:
1)功能独立——不依赖于程序的状态(比如可能发生变化的全局变量);
2)纯函数——同一个输入永远能得到同一个输出;
3)有限的副作用——可以严格地限制函数外部对状态的更改导致的状态变化。
const prepareTea = () => 'greenTea';
const getTea = (numOfCups) => {
const teaCups = [];
for(let cups = 1; cups <= numOfCups; cups += 1) {
const teaCup = prepareTea();
teaCups.push(teaCup);
}
return teaCups;
};
const tea4TeamFCC = getTea(40);
console.log(tea4TeamFCC);
了解函数式编程术语
Callbacks
是被传递到另一个函数中调用的函数。例如在filter
中,回调函数告诉 JavaScript 以什么规则过滤数组。
函数就像其他正常值一样,可以赋值给变量、传递给另一个函数,或从其它函数返回,这种函数叫做first class
函数。在 JavaScript 中,所有函数都是first class
函数。
将函数为参数或返回值的函数叫做higher order
函数。
当函数传递给另一个函数或从另一个函数返回时,那些传入或返回的函数可以叫做lambda
。
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);
了解使用命令式编程的危害
窗口对象由选项卡组成,通常会打开多个窗口。窗口对象中每个打开网站的标题都保存在一个数组中。在对浏览器进行了如打开新标签、合并窗口、关闭标签之类的操作后,你需要输出所有打开的标签。关掉的标签将从数组中删除,新打开的标签(为简单起见)则添加到数组的末尾。
var Window = function(tabs) {
this.tabs = tabs; // 我们将数组记录在对象内部
}; // tabs 是窗口中打开网站的标题数组
Window.prototype.join = function (otherWindow) {
this.tabs = this.tabs.concat(otherWindow.tabs);
return this;
}; // 当两个窗口合并成一个窗口时
Window.prototype.tabOpen = function (tab) {
this.tabs.push('new tab'); // 打开一个新的标签
return this;
}; // 在末尾打开一个新标签
Window.prototype.tabClose = function (index) {
var tabsBeforeIndex = this.tabs.splice(0, index); // 获取前面的标签
var tabsAfterIndex = this.tabs.splice(index); // 获取后面的标签
this.tabs = tabsBeforeIndex.concat(tabsAfterIndex); // 拼接到一起
return this;
}; // 关闭一个标签
// 创建三个浏览器窗口
var workWindow = new Window(['GMail', 'Inbox', 'Work mail', 'Docs', 'freeCodeCamp']); // 邮箱、文档及其他与工作相关的网站
var socialWindow = new Window(['FB', 'Gitter', 'Reddit', 'Twitter', 'Medium']); // 社交网站
var videoWindow = new Window(['Netflix', 'YouTube', 'Vimeo', 'Vine']); // 娱乐网站
var finalTabs = socialWindow
.tabOpen() // 新开一个 cat memes 的标签
.join(videoWindow.tabClose(2)) // 在娱乐网站关闭第三个标签,加入数组
.join(workWindow.tabClose(1).tabOpen());
alert(finalTabs.tabs); // 执行标签打开,关闭和其他操作
使用函数式编程避免突变和副作用:问题出在tabClose()函数里的splice。splice修改了调用它的原始数组,所以第二次调用它时是基于修改后的数组,才给出了意料之外的结果。
函数式编程的核心原则之一是不改变任何东西。变化会导致错误。如果一个函数不改变传入的参数、全局变量等数据,那么它造成问题的可能性就会小很多。
在函数式编程中,改变或变更叫做mutation
,这种改变的结果叫做“副作用”(side effect
)。理想情况下,函数应该是不会产生任何副作用的pure function
。
var fixedValue = 4;
function incrementer () {
return fixedValue + 1;
}
var newValue = incrementer(); // 应等于 5
console.log(fixedValue); // 应打印 4
传递参数以避免函数中的外部依赖
函数式编程的另一个原则是:总是显式声明依赖关系。如果函数依赖于一个变量或对象,那么将该变量或对象作为参数直接传递到函数中。
var fixedValue = 4;
function incrementer (value) {
return value + 1;
}
var newValue = incrementer(fixedValue); // 应等于 5
console.log(fixedValue); // 应打印 4
在函数中重构全局变量
// 全局变量
var bookList = ["The Hound of the Baskervilles", "On The Electrodynamics of Moving Bodies", "Philosophiæ Naturalis Principia Mathematica", "Disquisitiones Arithmeticae"];
function add (arr, bookName) {
let newArr = [...arr];
newArr.push(bookName);
return newArr;
}
function remove (arr, bookName) {
let newArr = [...arr];
if (newArr.indexOf(bookName) >= 0) {
newArr.splice(newArr.indexOf(bookName), 1);
return newArr; // Return the new array.
}
}
var newBookList = add(bookList, 'A Brief History of Time');
var newerBookList = remove(bookList, 'On The Electrodynamics of Moving Bodies');
var newestBookList = remove(add(bookList, 'A Brief History of Time'), 'On The Electrodynamics of Moving Bodies');
console.log(bookList);
// 等价于
function add (list,bookName) {
return [...list, bookName];
}
function remove (list,bookName) {
if (list.indexOf(bookName) >= 0) {
return list.filter((item) => item !== bookName);
}
}
使用 map 方法从数组中提取数据
函数在 JavaScript 中被视为First Class Objects
,它们可以像任何其他对象一样使用。它们可以保存在变量中,存储在对象中,也可以作为函数参数传递。
map
方法是迭代数组中每一项的方式之一。在对每个元素应用回调函数后,它会创建一个新数组(不改变原来的数组)。
rating = watchList.map( (item) => ({"title":item["Title"], "rating":item["imdbRating"]}) );
在原型上实现 map 方法:map
是一个纯函数,它的输出仅取决于输入的数组和作为参数传入的回调函数。纯函数可以改变其作用域内定义的局部变量,但我们最好不要这样做。
var 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])); }
// 等价于
// this.forEach(a => newArray.push(callback(a)));
return newArray;
};
var new_s = s.myMap(function(item){
return item * 2;
});
使用 filter 方法从数组中提取数据
另一个有用的数组方法是filter()
(即Array.prototype.filter()
)。filter
方法会返回一个长度不大于原始数组的新数组。
和map
一样,Filter
不会改变原始数组,它接收一个回调函数,将回调内的逻辑应用于数组的每个元素。新数组包含根据回调函数内条件返回 true 的元素。
var filteredList = watchList.map(function(e) {
return {title: e["Title"], rating: e["imdbRating"]}
}).filter((e) => e.rating >= 8);
在原型上实现 filter 方法:
var s = [23, 65, 98, 5];
Array.prototype.myFilter = function(callback){
var newArray = [];
for (let i=0; i<this.length;i++){
if(callback(this[i])=== true ){ newArray.push(this[i]); }
}
// 等价于
// this.forEach(function(x) { if (callback(x) == true) { newArray.push(x); } });
return newArray;
};
var new_s = s.myFilter(function(item){
return item % 2 === 1;
});
使用 slice 方法返回数组的一部分
slice
方法可以从已有数组中返回指定元素。它接受两个参数,第一个规定从何处开始选取,第二个规定从何处结束选取(不包括该元素)。如果没有传参,则默认为从数组的开头开始到结尾结束,这是复制整个数组的简单方式。slice
返回一个新数组,不会修改原始数组
。
var arr = ["Cat", "Dog", "Tiger", "Zebra"];
var newArray = arr.slice(1, 3); // 将新数组设置为 ["Dog", "Tiger"]
使用 slice 而不是 splice 从数组中移除元素:JavaScript 提供了splice
方法,它接收两个参数:从哪里开始删除项目的索引,和要删除的项目数。如果没有提供第二个参数,默认情况下是移除到结尾的元素。但splice
方法会改变调用它的原始数组。
var cities = ["Chicago", "Delhi", "Islamabad", "London", "Berlin"];
cities.splice(3, 1); // 返回 "London" 并将它从 cities 数组删除
// cities 现在是 ["Chicago", "Delhi", "Islamabad", "Berlin"]
使用 concat 方法组合两个数组
Concatenation
意思是将元素连接到尾部。同理,JavaScript 为字符串和数组提供了concat
方法。对数组来说,在一个数组上调用concat
方法,然后提供另一个数组作为参数添加到第一个数组末尾,返回一个新数组,不会改变任何一个原始数组。
[1, 2, 3].concat([4, 5, 6]); // 返回新数组 [1, 2, 3, 4, 5, 6]
使用 concat 而不是 push 将元素添加到数组的末尾:函数式编程就是创建和使用 non-mutating 函数。
var arr = [1, 2, 3];
arr.push([4, 5, 6]); // arr 变成了 [1, 2, 3, [4, 5, 6]] 不是函数式编程
使用 reduce 方法分析数据
reduce()
(即Array.prototype.reduce()
),是 JavaScript 所有数组操作中最通用的方法。几乎可以用reduce
方法解决所有数组处理问题。
filter
和map
方法不支持对数组中两个不同元素的交互。举个例子,如果你想把数组中的元素拿来比较或者相加,用filter
和map
是做不到的。
reduce
方法允许更通用的数组处理方式,而且filter
和map
方法都可以当作是reduce
的特殊实现。
var averageRating = watchList.filter(x => x.Director === "Christopher Nolan").map(x => Number(x.imdbRating)).reduce((x1, x2) => x1 + x2) / watchList.filter(x => x.Director === "Christopher Nolan").length;
使用 sort 方法按字母顺序给数组排序
在alphabeticalOrder函数中使用sort方法对arr中的元素按照字母顺序排列:
function alphabeticalOrder(arr) {
return arr.sort(function (a, b) {
if (a < b) return -1
else if (a > b) return 1
else return 0
});
}
alphabeticalOrder(["a", "d", "c", "a", "z", "g"]);
在不更改原始数组的前提下返回排序后的数组
concat返回一个新数组,再用sort方法:
var globalArray = [5, 6, 3, 2, 9];
function nonMutatingSort(arr) {
return [].concat(arr).sort(function(a,b){
if(a<b) return -1
else if(a>b) return 1
else return 0
});
}
nonMutatingSort(globalArray);
使用 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");
应用函数式编程将字符串转换为URL片段
var globalTitle = "Winter Is Coming";
function urlSlug(title) {
return title.split(/\W/).filter((obj)=>{
return obj !=='';
}).join('-').toLowerCase();
}
var winterComing = urlSlug(globalTitle); // 应为 "winter-is-coming"
使用 every 方法检查数组中的每个元素是否符合条件
使用every方法检查arr中是否所有元素都是正数:
function checkPositive(arr) {
return arr.every(function(a){
return a > 0;
});
}
checkPositive([1, 2, 3, -4, 5]);
使用 some 方法检查数组中是否有元素是否符合条件
使用some方法检查arr中是否所有元素都是正数:
function checkPositive(arr) {
return arr.some(function(a){
return a > 0;
});
}
checkPositive([1, 2, 3, -4, 5]);
函数柯里化
arity是函数所需的形参的数量。函数Currying意思是把接受多个arity的函数变换成接受单一arity的函数。换句话说,就是重构函数让它接收一个参数,然后返回接收下一个参数的函数,依此类推。
function unCurried(x, y) {
return x + y;
}
// 柯里化函数
function curried(x) {
return function(y) {
return x + y;
}
}
curried(1)(2) // 返回 3
var funcForY = curried(1);
console.log(funcForY(2)); // 打印 3
function impartial(x, y, z) {
return x + y + z;
}
var partialFn = impartial.bind(this, 1, 2);
partialFn(10); // 返回 13
function add(x) {
return function(y){
return function(z){
return x + y + z;
};
};
}
add(10)(20)(30);
网友评论