为什么要学习函数式编程
-
函数式编程是随着React的流行受到越来越多的关注。
就像当年谷歌的地图带火了ajax,react中的高阶组件就是使用了高阶函数来实现,高阶函数就是函数式编程的一个特性(后续会学习)我们这里需要注意一个点:虽然react使用了函数式编程的一些特性,但它并不是纯函数式的,另外react中的一些生态,比如redux它使用了函数式编程的思想,所以我们想要真正学好react和redux的话我们需要了解函数式编程 -
Vue3也开始拥抱函数式编程。
我们都知道Vue3对Vue2做了很大一部分的重构,而且越来越偏向函数式,比如Vue2中大量的使用了高阶函数,从而可以看出这些主流框架都越来越偏向函数式 -
函数式编程可以抛弃this。
很多同学在学习js前都接触过面向对象的语言,比如:java、c#、c++
等等,所以在学习js时我们大都是从面向对象开始学习的,我们会学习原型
、原型链
、模拟继承
来实现面向对象的一些特性,当然我们在学习中也会遇到烦人的this,在使用的过程中我们肯定也遇到过很多各种各样的问题,我们在使用js模拟面向对象是也非常的痛苦,那之后我们可以使用函数式编程,使用它我们甚至可以抛弃烦人的this -
打包过程中可以更好的利用tree shaking过滤无用代码
-
方便测试,方便并行处理
-
有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda
什么是函数式编程
函数式编程(Functional Programming,FP) FP是编程范式之一,他和面向对象编程是并列的关系,函数式编程我们可以认为是一种思维的方法,加上他的实现方法,我们常听说的编程范式还有面向过程编程、面向对象编程。
面向过程其实就是现实世界的处理方式,一步一步的按照步骤来实现
-
面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系
-
函数式编程的思维方式:把现实世界的事物和事物之间的
联系
抽象到程序世界(对运算过程进行抽象)-
程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
-
x --> f(联系、映射) -> y; y = f(x)
-
函数式编程中的函数指的不是程序中的函数(方法)
,而是数学中的函数即映射关系,例如:y = sin(x)
,x和y的关系 -
相同的输入始终要得到相同的输出
(纯函数) -
函数式编程用来描述数据(函数)之间的映射
// 非函数式 --- 面向过程编程方式 let num1 = 2 let num2 = 3 let sum = num1 + num2 console.log(sum) // 函数式 function add (n1, n2) { return n1 + n2 } let sum = add(2,3) console.log(sum)
-
-
从代码我们可以看出函数式编程提高了我们的代码重用性,而且在我们的编程中,将多个函数组合在一块可以组合出一个功能更强大的函数,这是非函数式编程所欠缺的
前置知识
- 函数是一等公民
- 高阶函数
- 闭包
函数是一等公民
MDN First-class Function (在以下三点解释)
* 函数可以存储在变量中
* 函数作为参数
* 函数作为返回值
在js中函数就是一个普通的对象
(可以通过new Function()),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过new Function(‘alert(1)’)来构造一个新的函数。
- 把函数赋值给变量
// 把函数赋值给变量
let fn = function(){
console.log('hello world')
};
fn();
// 一个示例
const BlogController = {
index (posts) { return Views.index(posts) },
show (post) { return Views.show(post) },
create (attrs) { return Db.create(attrs) },
update (post, attrs) { return Db.update(post, attrs) },
destroy (post) { return Db.destroy(post) },
}
// 如果我们将来遇到一个函数包裹一个函数,而且它的形式也相同时,我们可以认为,这是两个一样的函数,那我们就可以把这段代码进行一点精简,我们可以看到index和其内部的Views.index是一样的,所以我们可以把Views.index赋值给index,注意,我们是将一个方法或者函数赋值给另一个方法,不是把函数的调用赋值给另一个方法
// 当我们改造完成后,代码量将减少很多,功能也完全一样,这里我们就用到了将一个函数或者方法赋值给另一个函数或者方法
// 优化
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy
}
- 函数式一等公民是我们后面要学习的高阶函数、柯里化的基础
高阶函数
什么是高阶函数
-
高阶函数(Higher-order function) — 学过react的话会觉得名字和其中的高阶组件一样,其实react的高阶组件本质上就是高阶函数
-
可以把函数作为参数传递给另一个函数
-
可以把函数作为另一个函数的返回结果
-
-
函数作为参数
// 高阶函数---函数作为参数 // forEach function forEach (array,fn){ for(let i = 0; i < array.length; i++){ fn(array[i]) } }; // filter function filter (array,fn){ let results = []; for(let i = 0; i < array.length; i++){ if(fn(array[i])){ results.push(array[i]) } } return results; };
-
函数作为参数的好处
- 可以使我们的函数更灵活,调用forEach不需要考虑函数内部是如何实现的,该函数把内部实现的细节帮我们屏蔽了,而且名字都是符合见名知意的,比如forEach一看就是遍历的,filter一看就是过滤的
-
函数作为返回值
// 高阶函数---函数作为返回值
function fn(){
let msg = 'hello world';
return function (){
console.log(msg);
};
};
// 调用方式一
let func = fn();
func();
// 调用方式二
fn()();
// 实际案例:只执行一次的函数
function once(fn){
let done = true;
return function(){
if(done){
done = false;
return fn.apply(this,arguments);
// this --- 将自身this传递过去 --- 不写就是undefind
// arguments --- 将接受的参数传递过去 --- 不写就是undefind
};
};
};
let on = once(function(x){
console.log(`执行了${x}次`);
});
使用高阶函数的意义
对运算过程进行抽象,也就是把运算过程抽象成函数,然后在任何地方都可以去重用这些函数
-
抽象可以帮我们屏蔽细节,只需要关注我们的目标
-
高阶函数是用来抽象通用的问题
-
示例
//遍历一个数组
//面相过程的方式
let array = [1,2,3,4]
for(let i = 0; i < array.lenth; i++){
console.log(array[i])
}
//高阶函数
let array = [1,2,3,4]
forEach(array, item=>{
console.log(item)
})
let r = filter(array,item => {
return item % 2 === 0
})
- 意义
- 可以使函数更加灵活
- 抽象可以帮助我们屏蔽细节
常用高阶函数
- forEach
- filter
- map
- every
- some
- find / findIndex
- reduce
- sort
- …
我们之前已经模拟过forEach
、filter
,所以我们现在来模拟map
、every
、some
// map --- 数组中的一个方法,对数组中的每一个元素进行遍历,并对每一个元素进行一个处理,并对处理后的结果存储到一个新数组中然后返回
const map = (array,fn) => {
let results = [];
for(let value of array){
results.push(fn(value));
}
return results
};
let arr = [1,2,3,4];
arr = map(arr,v => v * v );
console.log(arr);
// every --- 遍历数组中的每一个元素,并对其做一个判断,其中有一个是false,其返回值就是false
const every = (array, fn) => {
let results = true;
for(let item of array){
results = fn(item);
if(!results){
break
}
};
return results;
};
let arr = [1,2,3,4];
let a = every(arr,v => v > 0);
console.log(a);
// some --- 遍历数组中的没一个元素,并对其做一个判断,其中有一个是true,其返回值就是true
const some = (array,fn) => {
let results = false;
for(let item of array){
results = fn(item);
if(results){
break;
};
};
return results;
};
let arr = [1,2,3,4];
let a = some(arr,v => v % 2 === 0);
console.log(a);
- 这三个方法都可以去接收一个函数,他们都是高阶函数,通过把一个函数传递给另一个函数,我们可以让这个函数变得更灵活
闭包
- 闭包(Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
- 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
- 其实闭包就是一个可以获取到其他函数内部变量的函数,他必须要满足三个条件才能被称之为闭包
- 首先要有一个函数
- 然后在这个函数内部去嵌套另一个函数
- 最后需要内层函数去访问到外部函数的变量
- 示例
function fn(){
let msg = 'hello world';
return function (){
console.log(msg);
};
};
let func = fn();
func();
function once(fn){
let done = true;
return function(){
if(done){
done = false;
return fn.apply(this,arguments);
};
};
};
let on = once(function(x){
console.log(`执行了${x}次`);
});
on(1);
- 核心作用:延长了外部函数内部变量的作用范围,换句话说就是延长了变量的存在时间
- 本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,
但是堆上的作用域成员因为被外部引用不能释放
,因此内部函数依然可以访问外部函数的成员 - 好处:延长了外部函数内部的变量的存在的作用范围
闭包的案例
// 案例一
function makePow(pow) {
return function (num) {
return Math.pow(num,pow)
}
}
// 求X次方
let pow2 = makePow(2);
let pow3 = makePow(3);
// 求值
console.log(pow2(3));
console.log(pow2(4));
console.log(pow3(3));
// 案例二
function wage(basic) {
return function (per) {
return basic + per
}
};
let twelve = wage(12000);
let fifteen = wage(15000);
console.log(`王二工资${twelve(2000)}RMB`);
console.log(`张三工资${fifteen(3000)}RMB`);