为什么要学习函数式编程
- 函数式编程是种编程范式,它主要关注的是运算过程本身,而忽略过程发生的环境。
- 函数式编程是声明式编程范式,它关注的是运算结果,而忽略运算过程。
函数式编程的特点
函数式编程(Functional Programming),FP 是编程范式之一,我们常听说的还有
面相过程,面相对象编程把现实世界的事物和事物之间的联系抽象到程序世界,对运算过程进行抽象
相同的输入,得到相同的输出(纯函数)
函数式编程是用来描述数据函数之间的关系
//非函数式
var a = 1;
var b = 2;
var c = a + b;
console.log(c);
// 函数式编程
var add = function (a, b) {
return a + b;
};
console.log(add(1, 2));
// 函数式编程
var add = (a, b) => a + b;
console.log(add(1, 2));
函数是第一公民
- 函数可以存储在变量中
//把函数赋值给变量
var add = function (a, b) {
return a + b;
};
add();
-函数可以作为参数传递给另一个函数
- 函数可以作为返回值返回给另一个函数;
高阶函数
- HOC(Higher-order function)
- 可以把函数作为另一个函数的返回值
//高阶函数
function myForEach(array, fn) {
for (var i = 0; i < array.length; i++) {
fn(array[i], i, array);
}
}
var arr = [1, 2, 3];
var fn = function () {
// console.log(arguments);
console.log(arguments[0]);
};
myForEach(arr, fn);
- 封装自定义的 filter
//封装自定义filter
function filter(array, fn) {
let newArr = [];
for (let i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
newArr.push(array[i]);
}
}
return newArr;
}
const res = filter(arr, (item) => item % 2 == 0);
console.log("🚀 ~ res:", res);
- 作为函数的返回值
//作为返回值返回
function makeFn() {
let str = "this is a string";
return function () {
console.log(str);
};
}
const testFn = makeFn();
testFn();
- 只触发一次的函数封装
//只触发一次
function once(cb) {
let isFlag = false;
return function () {
if (isFlag) return;
console.log("helloCitizen");
cb && cb.apply(this, arguments);
isFlag = true;
};
}
const testOnce = once(function () {
const args = arguments;
console.log("hello", ...arguments);
});
testOnce("a", "b"); // 这里会执行
testOnce("c", "d"); //这里就不会执行
testOnce("e", "f"); //这里也不会执行
/**
* helloCitizen
hello a b
*/
使用高阶函数的意义
- 函数式编程的精髓就是高阶函数,它可以让代码更加简洁,可读性更高。
- 屏蔽实现细节,只需关注我们的业务逻辑。目标
// 函数式编程
let arr1 = [11, 22, 32];
forEach(arr1, (item) => {
console.log(item);
});
function forEach(array, fn) {
for (let index = 0; index < array.length; index++) {
fn && fn(array[index], index, array);
}
}
常用的高阶函数
- map,every,some,filter
//模拟实现map
function map(array, fn) {
for (let index = 0; index < array.length; index++) {
array[index] = fn(array[index], index, array);
}
return array;
}
let array2 = [1, 2, 3];
const res2 = map(array2, (item) => item * 10);
console.log("🚀 ~ res2:", res2); //🚀 ~ res2: [ 10, 20, 30 ]
//模拟实现every
function every(array, fn) {
for (let index = 0; index < array.length; index++) {
if (!fn(array[index], index, array)) {
return false;
}
}
return true;
}
let array3 = [1, 2, 3];
const res3 = every(array3, (item) => item % 2 == 0);
console.log("🚀 ~ res3:", res3);
const res4 = every(array3, (item) => item > 0);
console.log("🚀 ~ res3:", res4);
//🚀 ~ res3: false
// 🚀 ~ res3: true
//模拟实现some
function some(array, fn) {
for (let index = 0; index < array.length; index++) {
if (fn(array[index], index, array)) {
return true;
}
}
return false;
}
let array5 = [1, 2, 3];
const res5 = some(array5, (item) => item % 2 == 0);
console.log("🚀 ~ res5:", res5);
//🚀 ~ res5: true
闭包
- 闭包就是能够读取其他函数内部变量的函数。
- 闭包就是函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 中的变量。
- 这些变量也叫自由变量
function maxPower(power) {
return function (num) {
return Math.pow(num, power);
};
}
const fn1 = maxPower(2);
const res6 = fn1(3);
console.log("🚀 ~ res6:", res6);
纯函数
相同的输入始终会得到相同的输出
lodash 库的函数都是纯函数
纯函数的好处就是可以缓存结果,避免重复计算
slice 是纯函数,但是 concat,splice 不是
相同的输入,永远得到相同的输出,那我们就可以缓存结果,避免重复计算
lodash 库的几个函数测试
const array = [1, 2, 3];
const first = _.head(array);
console.log("🚀 ~ first:", first); // first: 1
const last = _.last(array);
console.log("🚀 ~ last:", last); // first: 3
_([1, 2]).forEach(function (value) {
console.log(value);
});
const res1 = _.groupBy([1, 2, 3, 4, 5], (value) => value % 2 == 0);
console.log("🚀 ~ res1:", res1);
const res2 = _.includes([1, 2, 3], 2);
console.log("🚀 ~ res2:", res2);
const res3 = _.includes({ user: "barney", age: 36, active: true }, "barney");
console.log("🚀 ~ res3:", res3);
const res4 = _.invokeMap(
[
[3, 2, 4, 5],
[5, 2, 7, 3, 1],
],
"sort"
);
console.log("🚀 ~ res4:", res4);
- 模拟实现纯函数的缓存作用
function getArea(r) {
console.log(r);
return Math.PI * r * r;
}
var object = { a: 1, b: 2 };
var other = { c: 3, d: 4 };
//范式1
const res1 = _.memoize(getArea);
const values = res1(6);
console.log("🚀 ~ values:", values);
//范式2
const res2 = _.memoize(_.values);
const values2 = res2(object);
console.log("🚀 ~ values2:", values2);
object.a = 2;
const values3 = res2(object);
console.log("🚀 ~ values3:", values3);
//实现纯函数的缓存
const res1 = _.memoize(getArea);
const values = res1(6);
const values2 = res1(6);
const values3 = res1(6);
console.log("🚀 ~ values:", values);
console.log("🚀 ~ values:", values);
console.log("🚀 ~ values:", values);
/**
* 6
demo3.js:26 🚀 ~ values: 113.09733552923255
demo3.js:27 🚀 ~ values: 113.09733552923255
demo3.js:28 🚀 ~ values: 113.09733552923255
输入三次6,输出三次113.09733552923255,但是只调用了getArea函数一次,说明纯函数缓存起作用了
*/
function myMemoize(fn) {
let cache = {};
return function () {
const arg = JSON.stringify(arguments);
if (cache[arg]) {
return cache[arg];
}
const res = fn.apply(this, arguments);
cache[arg] = res;
return res;
};
}
//优化myMemoize
function myMemoize1(fn) {
let cache = {};
return function () {
const arg = JSON.stringify(arguments);
cache[arg] = cache[arg] || fn.apply(this, arguments);
return cache[arg];
};
}
const test = myMemoize(getArea);
const res1 = test(6);
const res2 = test(6);
const res3 = test(6);
console.log("🚀 ~ res1:", res1);
console.log("🚀 ~ res2:", res2);
console.log("🚀 ~ res3:", res3);
const test1 = myMemoize1(getArea);
const opp1 = test1(6);
const opp2 = test1(6);
const opp3 = test1(6);
console.log("🚀 ~ opp1:", opp1);
console.log("🚀 ~ opp2:", opp2);
console.log("🚀 ~ opp3:", opp3);
/**
* 6
demo3.js:66 🚀 ~ res1: 113.09733552923255
demo3.js:67 🚀 ~ res2: 113.09733552923255
demo3.js:68 🚀 ~ res3: 113.09733552923255
demo3.js:2 6
demo3.js:74 🚀 ~ opp1: 113.09733552923255
demo3.js:75 🚀 ~ opp2: 113.09733552923255
demo3.js:76 🚀 ~ opp3: 113.09733552923255
*/
副作用
- 函数式编程中,副作用指的是函数执行过程中,除了返回函数值之外,还对外部产生了可见的影响。
- 如果一个函数依赖外部的状态,那么这个函数就是有副作用的,无法保证相同的输出
//不纯的函数
let min = 18;
function checkAge(age) {
return age >= min;
}
- 副作用的来源
- 函数内部访问了全局变量
- 函数内部修改了全局变量
- 函数内部调用了其他函数
- 函数内部调用了 I/O 操作
- 函数内部调用了 Date.now()
- 函数内部调用了 Math.random()
- 函数内部调用了 setTimeout()
- 函数内部调用了 setInterval()
- 函数内部调用了 setImmediate()
- 函数内部调用了 process.nextTick()
柯里化
- 柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果
- 柯里化是把一个多参数的函数转换成多个单参数的函数,且返回值是一个函数。
- 柯里化是函数式编程的一个重要的概念,它把一个多参数的函数转换成多个单参数的函数。
//硬编码,不提倡,反例
function checkAge(age) {
let min = 18;
return age > min;
}
//普通的纯函数
function checkAge(min, age) {
return age > min;
}
//柯里化
function checkAge2(min) {
return function (age) {
return age > min;
};
}
//es6语法
const checkAge2 = (min) => (age) => age > min;
const checkAge18 = checkAge2(18);
console.log(checkAge18(20)); //true
const checkAge20 = checkAge2(20);
console.log(checkAge20(18)); //false
- 模拟实现柯里化的原理
//lodash中的纯函数
const abc = function (a, b, c) {
return a + b + c;
};
const curried = _.curry(abc);
console.log(curried(1)(2)(3)); //6
console.log(curried(1, 2)(3)); //6
console.log(curried(1)(2, 3)); //6
console.log(curried(1, 2, 3)); //6
//abc形参的个数
console.log(abc.length); //3
//模拟实现柯里化
function myCurry(fn) {
return function myCurried(...rest) {
if (rest.length < fn.length) {
return function () {
return myCurried(...[...rest, ...arguments]);
};
}
return fn.apply(this, rest);
};
}
const res2 = myCurry(abc);
console.log("mycurry", res2(1)(2, 3)); //6
console.log("mycurry", res2(1, 2)(3)); //6
console.log("mycurry", res2(1)(2, 3)); //6
console.log("mycurry", res2(1, 2, 3)); //6
console.log("mycurry", res2(1)(2)(3)); //6
函数组合
- 函数组合指的是把多个函数组合成一个新的函数,新函数的计算结果,是各个函数运算后的结果结合在一起。
- 函数组合的目的是把多个函数运算的结果,合并成一个结果。
function compose(f, g) {
return function (value) {
return f(g(value));
};
}
const add = (x) => x + 1;
const multiply = (x) => x * 2;
const getData = compose(add, multiply);
const res = getData(1);
console.log(res);
- 模拟封装组合函数
//函数组合
function compose(f, g) {
return function (value) {
return f(g(value));
};
}
const add = (x) => x + 1;
const multiply = (x) => x * 2;
const add2 = (x) => x + 100;
const join = (x) => x + x;
const getData = compose(add, multiply);
const res = getData(1);
console.log(res);
//模拟函数组合
function myCompose() {
var args = Array.prototype.slice.call(arguments);
return function () {
var result = args[0].apply(this, arguments);
for (let i = 1; i < args.length; i++) {
result = args[i](result);
}
return result;
};
}
//基础版本
function myCompose1(...args) {
return function (...payload) {
var result = payload[0];
for (let i = args.length - 1; i >= 0; i--) {
result = args[i](result);
}
return result;
};
}
//改进版
function myCompose2(...args) {
return function (value) {
return args.reverse().reduce((prev, fn) => fn(prev), value);
};
}
//升级版
const myCompose3 =
(...args) =>
(value) =>
args.reverse().reduce((prev, fn) => fn(prev), value);
const getData2 = myCompose1(add, multiply, add2);
const res2 = getData2(1);
console.log("res2", res2); // 203
const getData3 = myCompose2(add, multiply, add2, join);
const res3 = getData3(1);
console.log("res3", res3); // 203
const getData4 = myCompose3(add, multiply, add2, join);
const res4 = getData4(1);
console.log("res4", res4); // 205