在长时间学习和使用面向对象编程之后,咱们退一步来考虑系统复杂性。
在做了一些研究之后,我发现了函数式编程的概念,比如不变性和纯函数。这些概念使你能够构建无副作用的函数,因此更容易维护具有其他优点的系统。
在这篇文章中,将通大量代码示例来详细介绍函数式编程和一些相关重要概念。
函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数学函数的评估,避免了状态的变化和数据的可变。
当我们想要理解函数式编程时,需要知道的第一个基本概念是纯函数,但纯函数又是什么鬼?
咱们怎么知道一个函数是否是纯函数?这里有一个非常严格的定义:
如果给出相同的参数,它返回相同的结果。 想象一下,我们想要实现一个计算圆的面积的函数。
不是纯函数会这样做,接收radius 作为参数,然后计算radius * radius * PI:
let PI = 3.14;
const calculateArea = (radius) => radius * radius * PI;
calculateArea(10); // returns 314.0
为什么这是一个不纯函数?原因很简单,因为它使用了一个没有作为参数传递给函数的全局对象。
现在,想象一些数学家认为圆周率的值实际上是42并且修改了全局对象的值。
不纯函数得到10 * 10 * 42 = 4200。对于相同的参数(radius = 10),我们得到了不同的结果。
修复它:
let PI = 3.14;
const calculateArea = (radius, pi) => radius * radius * pi;
calculateArea(10, PI); // returns 314.0
现在把 PI 的值作为参数传递给函数,这样就没有外部对象引入。
下面函数读取外部文件,它不是纯函数,文件的内容随时可能都不一样。
const charactersCounter = (text) => `Character count: ${text.length}`;
function analyzeFile(filename) {
let fileContent = open(filename);
return charactersCounter(fileContent);
}
任何依赖于随机数生成器的函数都不能是纯函数。
function yearEndEvaluation() {
if (Math.random() > 0.5) {
return "You get a raise!";
} else {
return "Better luck next year!";
}
}
纯函数不会引起任何可观察到的副作用。可见副作用的例子包括修改全局对象或通过引用传递的参数。
现在,咱们要实现一个函数,该接收一个整数并返对该整数进行加1操作且返回。
let counter = 1;
function increaseCounter(value) {
counter = value + 1;
}
increaseCounter(counter);
console.log(counter); // 2
该非纯函数接收该值并重新分配counter,使其值增加1。
函数式编程不鼓励可变性。我们修改全局对象,但是要怎么做才能让它变得纯函数呢?只需返回增加1的值。
let counter = 1;
const increaseCounter = (value) => value + 1;
increaseCounter(counter); // 2
console.log(counter); // 1
纯函数increaseCounter返回2,但是counter值仍然是相同的。函数返回递增的值,而不改变变量的值。
如果我们遵循这两条简单的规则,就会更容易理解我们的程序。现在每个函数都是孤立的,不能影响系统的其他部分。
纯函数是稳定的、一致的和可预测的。给定相同的参数,纯函数总是返回相同的结果。
咱们不需要考虑相同参数有不同结果的情况,因为它永远不会发生。
纯函数代码肯定更容易测试,不需要 mock 任何东西,因此,我们可以使用不同的上下文对纯函数进行单元测试:
一个简单的例子是接收一组数字,并对每个数进行加 1 这种沙雕的操作。
let list = [1, 2, 3, 4, 5];
const incrementNumbers = (list) => list.map(number => number + 1);
接收numbers数组,使用map递增每个数字,并返回一个新的递增数字列表。
incrementNumbers(list); // [2, 3, 4, 5, 6]
对于输入[1,2,3,4,5],预期输出是[2,3,4,5,6]。
尽管时间变或者不变,纯函数大佬都是不变的。
当数据是不可变的时,它的状态在创建后不能更改。
咱们不能更改不可变对象,如果非要来硬的,刚需要深拷贝一个副本,然后操作这个副本。
在JS中,我们通常使用for循环,for的每次遍历 i是个可变变量。
var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;
for (var i = 0; i < values.length; i++) {
sumOfValues += values[i];
}
sumOfValues // 15
对于每次遍历,都在更改i和sumOfValue状态,但是我们如何在遍历中处理可变性呢? 答案就是使用递归。
let list = [1, 2, 3, 4, 5];
let accumulator = 0;
function sum(list, accumulator) {
if (list.length == 0) {
return accumulator;
}
return sum(list.slice(1), accumulator + list[0]);
}
sum(list, accumulator); // 15
list; // [1, 2, 3, 4, 5]
accumulator; // 0
上面代码有个 sum 函数,它接收一个数值向量。函数调用自身,直到 list为空退出递归。对于每次“遍历”,我们将把值添加到总accumulator中。
使用递归,咱们保持变量不变。不会更改list和accumulator变量。它保持相同的值。
观察:我们可以使用reduce来实现这个功能。这个在接下的高阶函数内容中讨论。
构建对象的最终状态也很常见。假设我们有一个字符串,想把这个字符串转换成url slug。
在Ruby的面向对象编程中,咱们可以创建一个类 UrlSlugify,这个类有一个slugify方法来将字符串输入转换为url slug。
class UrlSlugify
attr_reader :text
def initialize(text)
@text = text
end
def slugify!
text.downcase!
text.strip!
text.gsub!(' ', '-')
end
end
UrlSlugify.new(' I will be a url slug ').slugify! # "i-will-be-a-url-slug"
上面使用的有命令式编程方式,首先用小写字母表示我们想在每个slugify进程中做什么,然后删除无用的空格,最后用连字符替换剩余的空格。
这种方式在整个过程中改变了输入状态,显然不符合纯函数的概念。
这边可以通过函数组合或函数链来来优化。换句话说,函数的结果将用作下一个函数的输入,而不修改原始输入字符串。
const string = " I will be a url slug ";
const slugify = string =>
string
.toLowerCase()
.trim()
.split(" ")
.join("-");
slugify(string); // i-will-be-a-url-slug
上述代码主要做了这几件事:
接着实现一个square 函数:
const square = (n) => n * n;
给定相同的输入,这个纯函数总是有相同的输出。
square(2); // 4
square(2); // 4
square(2); // 4
// ...
将2作为square函数的参数传递始终会返回4。这样咱们可以把square(2)换成4,我们的函数就是引用透明的。
基本上,如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。
有了这个概念,咱们可以做的一件很酷的事情就是记住这个函数。假设有这样的函数
const sum = (a, b) => a + b;
用这些参数来调用它
sum(3, sum(5, 8));
sum(5, 8) 总等于13,所以可以做些骚操作:
sum(3, 13);
这个表达式总是得到16,咱们可以用一个数值常数替换整个表达式,并把它记下来。
函数作为 JS 中的一级公民,很风骚,函数也可以被看作成值并用作数据使用。
其思想是将函数视为值,并将函数作为数据传递。通过这种方式,我们可以组合不同的函数来创建具有新行为的新函数。
假如我们有一个函数,它对两个值求和,然后将值加倍,如下所示:
const doubleSum = (a, b) => (a + b) * 2;
对应两个值求差,然后将值加倍:
const doubleSubtraction = (a, b) => (a - b) * 2;
这些函数具有相似的逻辑,但区别在于运算符的功能。 如果我们可以将函数视为值并将它们作为参数传递,我们可以构建一个接收运算符函数并在函数内部使用它的函数。
const sum = (a, b) => a + b;
const subtraction = (a, b) => a - b;
const doubleOperator = (f, a, b) => f(a, b) * 2;
doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4
f参数并用它来处理a和b, 这里传递了sum函数和subtraction并使用doubleOperator函数进行组合并创建新行为。
当我们讨论高阶函数时,通常包括以下几点:
上面实现的doubleOperator函数是一个高阶函数,因为它将一个运算符函数作为参数并使用它。
我们经常用的filter、map和reduce都是高阶函数,Look see see。
对于给定的集合,我们希望根据属性进行筛选。filter函数期望一个true或false值来决定元素是否应该包含在结果集合中。
如果回调表达式为真,过滤器函数将在结果集合中包含元素,否则,它不会。
一个简单的例子是,当我们有一个整数集合,我们只想要偶数。
使用命令式方式来获取数组中所有的偶数,通常会这样做:
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var evenNumbers = [];
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 == 0) {
evenNumbers.push(numbers[i]);
}
}
console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]
我们还可以使用filter高阶函数来接收偶函数并返回一个偶数列表:
const even = n => n % 2 == 0;
const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]
我在 Hacker Rank FP 上解决的一个有趣问题是Filter Array问题。 问题是过滤给定的整数数组,并仅输出小于指定值X的那些值。
命令式做法通常是这样的:
var filterArray = function(x, coll) {
var resultArray = [];
for (var i = 0; i < coll.length; i++) {
if (coll[i] < x) {
resultArray.push(coll[i]);
}
}
return resultArray;
}
console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]
对于上面的总是,我们更想要一种更声明性的方法来解决这个问题,如下所示:
function smaller(number) {
return number < this;
}
function filterArray(x, listOfNumbers) {
return listOfNumbers.filter(smaller, x);
}
let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0];
filterArray(3, numbers); // [2, 1, 0]
在smaller的函数中使用 this,一开始看起来有点奇怪,但是很容易理解。
filter函数中的第二个参数表示上面 this, 也就是 x 值。
我们也可以用map方法做到这一点。想象一下,有一组信息
let people = [
{ name: "TK", age: 26 },
{ name: "Kaio", age: 10 },
{ name: "Kazumi", age: 30 }
]
我们希望过滤 age 大于 21 岁的人,用 filter 方式
const olderThan21 = person => person.age > 21;
const overAge = people => people.filter(olderThan21);
overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]
map函数的主要思路是转换集合。
map方法通过将函数应用于其所有元素并根据返回的值构建新集合来转换集合。
假如我们不想过滤年龄大于 21 的人,我们想做的是显示类似这样的:TK is 26 years old.
使用命令式,我们通常会这样做:
var people = [
{ name: "TK", age: 26 },
{ name: "Kaio", age: 10 },
{ name: "Kazumi", age: 30 }
];
var peopleSentences = [];
for (var i = 0; i < people.length; i++) {
var sentence = people[i].name + " is " + people[i].age + " years old";
peopleSentences.push(sentence);
}
console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
声明式会这样做:
const makeSentence = (person) => `${person.name} is ${person.age} years old`;
const peopleSentences = (people) => people.map(makeSentence);
peopleSentences(people);
// ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
整个思想是将一个给定的数组转换成一个新的数组。
另一个有趣的HackerRank问题是更新列表问题。我们想要用一个数组的绝对值来更新它的值。
例如,输入[1,2,3,- 4,5]需要输出为[1,2,3,4,5],-4的绝对值是4。
一个简单的解决方案是每个集合中值的就地更新,很危险的作法
var values = [1, 2, 3, -4, 5];
for (var i = 0; i < values.length; i++) {
values[i] = Math.abs(values[i]);
}
console.log(values); // [1, 2, 3, 4, 5]
我们使用Math.abs函数将值转换为其绝对值并进行就地更新。
这种方式不是最做解。
首先,前端我们学习了不变性,知道不可变性让函数更加一致和可预测,咱们的想法是建立一个具有所有绝对值的新集合。
其次,为什么不在这里使用map来“转换”所有数据
我的第一个想法是测试Math.abs函数只处理一个值。
Math.abs(-1); // 1
Math.abs(1); // 1
Math.abs(-2); // 2
Math.abs(2); // 2
我们想把每个值转换成一个正值(绝对值)。
现在知道如何对一个值执行绝对值操作,可以使用此函数作为参数传递给map函数。
还记得高阶函数可以接收函数作为参数并使用它吗? 是的,map函数可以做到这一点
let values = [1, 2, 3, -4, 5];
const updateListMap = (values) => values.map(Math.abs);
updateListMap(values); // [1, 2, 3, 4, 5]
reduce函数的思想是接收一个函数和一个集合,并返回通过组合这些项创建的值。
常见的的一个例子是获取订单的总金额。
假设你在一个购物网站,已经将产品1、产品2、产品3和产品4添加到购物车(订单)中。现在,我们要计算购物车的总数量:
以命令式的方式,就是便利订单列表并将每个产品金额与总金额相加。
var orders = [
{ productTitle: "Product 1", amount: 10 },
{ productTitle: "Product 2", amount: 30 },
{ productTitle: "Product 3", amount: 20 },
{ productTitle: "Product 4", amount: 60 }
];
var totalAmount = 0;
for (var i = 0; i < orders.length; i++) {
totalAmount += orders[i].amount;
}
console.log(totalAmount); // 120
使用reduce,我们可以构建一个函数来处理量计算sum并将其作为参数传递给reduce函数。
let shoppingCart = [
{ productTitle: "Product 1", amount: 10 },
{ productTitle: "Product 2", amount: 30 },
{ productTitle: "Product 3", amount: 20 },
{ productTitle: "Product 4", amount: 60 }
];
const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount;
const getTotalAmount = (shoppingCart) => shoppingCart.reduce(sumAmount, 0);
getTotalAmount(shoppingCart); // 120
这里有shoppingCart,接收当前currentTotalAmount的函数sumAmount,以及对它们求和的order对象。
咱们也可以使用map将shoppingCart转换为一个amount集合,然后使用reduce函数和sumAmount函数。
const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;
function getTotalAmount(shoppingCart) {
return shoppingCart
.map(getAmount)
.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 120
getAmount接收product对象并只返回amount值,即[10,30,20,60],然后,reduce通过相加将所有项组合起来。
看了每个高阶函数的工作原理。这里为你展示一个示例,说明如何在一个简单的示例中组合这三个函数。
说到购物车,假设我们的订单中有这个产品列表
let shoppingCart = [
{ productTitle: "Functional Programming", type: "books", amount: 10 },
{ productTitle: "Kindle", type: "eletronics", amount: 30 },
{ productTitle: "Shoes", type: "fashion", amount: 20 },
{ productTitle: "Clean Code", type: "books", amount: 60 }
]
假如相要想要购物车里类型为 books的总数,通常会这样做:
let shoppingCart = [
{ productTitle: "Functional Programming", type: "books", amount: 10 },
{ productTitle: "Kindle", type: "eletronics", amount: 30 },
{ productTitle: "Shoes", type: "fashion", amount: 20 },
{ productTitle: "Clean Code", type: "books", amount: 60 }
]
const byBooks = (order) => order.type == "books";
const getAmount = (order) => order.amount;
const sumAmount = (acc, amount) => acc + amount;
function getTotalAmount(shoppingCart) {
return shoppingCart
.filter(byBooks)
.map(getAmount)
.reduce(sumAmount, 0);
}
getTotalAmount(shoppingCart); // 70
原文:https://medium.com/better-programming/introduction-to-the-basic-principles-of-functional-programming-in-javascript-6849ae196326
如果你真的做出了一些东西,在面对那些令人眼花缭乱的理论知识,或是和你相似甚至比你做的更糟糕的人时大可不必谦虚。在一天结束之时,正是那些在战壕中的开发者——构建、测试和开发了代码的人,真正做了事情。
这些事情可以帮助新手在他们漫长的旅程中学习编程。我知道我还有更多东西需要学习,并将继续学习如何永远地学习。最重要的事情说三遍,请继续,不要放弃,不要放弃,不要放弃。
Javascript代码异步执行的场景,比如ajax的调用、定时器的使用等,在这样的场景下也经常会出现这样那样匪夷所思的bug或者糟糕的代码片段,那么处理好你的Javascript异步代码成为了异步编程至关重要的前提
以买苹果为例说明程序员如何解决问题。程序员需要对问题进行透彻的分析,理清其涉及的所有细节,预测可能发生的所有意外与非意外的情况,列出解决方案的所有步骤,以及对解决方案进行尽量全面的测试。而这些正是我认为编程难的地方。
Google Blockly 是一款基于Web的、开源的、可视化程序编辑器。你可以通过拖拽块的形式快速构建程序,而这些所拖拽的每个块就是组成程序的基本单元。可视化编程完成
成为伟大的程序员,需要付出许多编程之外的努力。我们的大脑是有限的,每天要应付的问题复杂到足以让人精神崩溃。当工作不顺利时,多少都会有些冒名顶替症候群的感觉。
推荐8款最好用的前端开发工具供美工或者前端开发人员使用,当然若你是NB的全栈工程师也可以下载使用。Web前端开发最常见的编程软件有以下几种: 在前端开发中,有一个非常好用的工具,Visual Studio Code,简称VS code
学编程现在看起来挺简单,因为网上有丰富的各种资源。然而当你实际去学的时候就发现,还是很难!对我来说也一样。但从某天起,我决定认认真真学编程一年。后来又过了一年,又过了一年又一年……我好像有点感悟。
命名最好遵循驼峰法和下划线法,并且要清楚的表达变量的意思。相对于驼峰法而言,我更喜欢下划线法。下划线法可以更清楚的看出这个变量表示的意思。比如aBigGreenBanana和一个a_big_green_banana。
每隔几个月就会出现一篇文章表明:CSS并不是真正的编程语言。以编程语言的标准来说,CSS过于困难。使用这门语言会很有创造性:事实确实如此,CSS不同于传统的编程,且具有缺陷,同任何标准化编程语言相比
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!