Function 相关知识
apply,bind,call 作用及区别
三者的区别
call
和apply
改变了函数
的 this上下文
后便执行该函数 ,bind
则是返回改变了上下文
后的一个函数
call
和bind
都可以接收多个参数,apply
接收两个参数 ,第二个参数为数组call
和apply
都是立即执行,bind
是返回一个函数(需要手动调用)
call
(可接收若干参数
)
- 调用 call 的对象,必须是个函数 Function.
- call 的第一个参数是一个对象,Function 的调用者,将会指向这个对象.如果不传,则默认为全局对象 window.
- 第二个参数开始,可以接受任意个参数. 每个参数会映射到相应位置的 Function 的参数上.但是如果将所有的参数作为数组传入,他们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空.
写法:
Function.call(obj,[param1[,param2[,…[,paramN]]]])
案例解释
// 基本使用
function person() {
console.log(this.name); // iike
}
let coco = {
name: "iike",
};
person.call(coco);
// 等同于
let coco2 = {
name: "iike",
person: function () {
console.log(this.name); // iike
},
};
coco2.person();
模拟实现 call 方法
function alias(a, b, c, d) {
console.log(this.name); // 元子啊
console.log(a, b, c, d); // [ '点赞', '评论', '关注', '收藏' ]
}
// 模拟实现call方法
Function.prototype.newCall = function (obj) {
console.log(this);
// obj = obj || window;
obj = obj || {};
obj.fn = this;
let newArguments = [];
for (let i = 1; i < arguments.length; i++) {
newArguments.push(`arguments[${i}]`);
}
// console.log(newArguments); // [ 'arguments[1]', 'arguments[2]', 'arguments[3]', 'arguments[4]' ]
// console.log(newArguments.toString()); // 'arguments[1],arguments[2],arguments[3],arguments[4]'
// console.log(`${newArguments}`); // 'arguments[1],arguments[2],arguments[3],arguments[4]'
let result = eval(`obj.fn(${newArguments})`);
delete obj.fn;
return result;
};
let iike = {
name: "元子啊",
};
alias.newCall(iike, "点赞", "评论", "关注", "收藏");
// alias.newCall(null, "点赞", "评论", "关注", "收藏"); // 传入null时, this 指向 当前窗口
apply
(可接收两个参数
)
- 它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
第二个参数,必须是数组或者类数组
,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
写法:
Function.apply(obj[,argArray])
模拟实现 apply 方法
function alias(a, b, c, d) {
// console.log(this.name); // 元子啊
// console.log(a, b, c, d); // [ '点赞', '评论', '关注', '收藏' ]
return {
name: this.name,
a,
b,
c,
d,
};
}
Function.prototype.newApply = function (context, args) {
context = context || window;
context.fn = this;
let result = null;
if (!args) {
// 如果没有参数,直接执行
result = context.fn();
} else {
let newArguments = [];
for (let i = 0; i < args.length; i++) {
newArguments.push(`args[${i}]`);
}
result = eval(`context.fn(${newArguments})`);
}
delete context.fn;
return result;
};
let iike = {
name: "元子啊",
};
// alias.newApply(iike);
let cr = alias.newApply(iike, ["点赞", "评论", "关注", "收藏"]);
console.log(cr); //{ name: '元子啊', a: '点赞', b: '评论', c: '关注', d: '收藏' }
bind
(手动调用)
- bind 方法会
返回一个新函数
,并且需要后面手动调用
模拟实现 bind 方法
function alias(a, b, c, d, e) {
console.log(this.name);
console.log(a, b, c, d); // [ '点赞', '评论', '关注', '收藏' ]
console.log(e); // 充电
}
Function.prototype.newBind = function (obj) {
if (typeof this !== "function") {
throw new Error("type error");
}
let that = this,
arr = Array.prototype.slice.call(arguments, 1),
o = function () {},
newf = function () {
let arr2 = Array.prototype.slice.call(arguments),
arrSum = arr.concat(arr2);
if (this instanceof o) {
this.apply(this, arrSum);
} else {
that.apply(obj, arrSum);
}
};
o.prototype = that.prototype;
newf.prototype = new o();
return newf;
};
let iike = {
name: "元子啊",
};
alias.bind(iike, "点赞", "评论", "关注", "收藏")("充电");
Function 函数式编程
函数是一等公民
- 函数可以作为参数传递
- 函数可以作为返回值
- 函数可以作为变量赋值
- 一等公民指的是函数与其他数据类型一样,处于平等地位,能参与运算,也可以作为参数传递,赋值等。
- 代码优化案例
// 优化前
let functionInvoke = {
index(posts) {
return views.index(posts);
},
show(pic) {
return views.show(pic);
},
};
// 优化后
let functionInvoke = {
index: views.index,
show: views.show,
};
高阶函数
- 接受一个或者多个函数作为其入参
- 返回值是一个函数
- 两者满足其一,则为高阶函数
- 使用高阶函数的意义
- 抽象可以帮我们屏蔽一些细节,只需要关注目标
- 高阶函数是用来抽象通用的问题
- 函数作为参数
案例
模拟常用的高阶函数
// 模拟常用高阶函数 (函数作为参数)
function filter(arr, fn) {
let results = [];
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
results.push(arr[i]);
}
}
return results;
}
console.log(filter([1, 2, 3, 4], (item) => item % 2 === 0)); // [2,4]
function map(arr, fn) {
let results = [];
for (let value of arr) {
results.push(fn(value));
}
return results;
}
console.log(map([2, 3, 4, 5, 6, 7], (item) => item * 2)); // [ 4, 6, 8, 10, 12, 14 ]
function every(arr, fn) {
let result = true;
for (let value of arr) {
result = fn(value);
if (fn(value) === false) break;
}
return result;
}
console.log(every([5, 6, 7, 8], (num) => num >= 4)); // true
console.log(every([5, 6, 7, 8], (num) => num > 5)); // false
- 函数作为返回值
案例
基础案例理解
// 函数作为返回值
function makeFn() {
let msg = "world";
return function () {
console.log("hello" + msg);
};
}
let fn = makeFn();
fn(); // helloworld
封装 once 函数, 使其只能被调用一次
function once(fn) {
let done = false;
return function () {
if (!done) {
done = true;
fn.apply(this, arguments);
}
};
}
let pay = once(function (money) {
console.log(`请支付${money}元.`);
});
pay(66); // 请支付66元.
pay(88); // '不执行 !!!'
闭包
传统概念:
闭包: 函数和其周围状态的引用捆绑在一起,形成闭包
通俗来讲:
一个可以访问另一个函数内部变量的函数就叫做闭包
(一个函数可以访问另一个函数内部的变量,这就叫闭包)- 比如: 函数 b 可以访问 函数 a 内部的变量,即 函数 b 就是 函数 a 的闭包.
案例解释:
makePower2TF
函数可以访问到makePowerTransform
函数中的变量,所以形成闭包
// 求指数
console.log(Math.pow(2, 3)); // 8
console.log(Math.pow(3, 3)); // 27
console.log(Math.pow(4, 3)); // 64
console.log(Math.pow(4, 2)); // 16
function makePower2(num) {
return Math.pow(num, 2);
}
function makePower3(num) {
return Math.pow(num, 3);
}
function makePower4(num) {
return Math.pow(num, 4);
}
console.log(makePower2(2)); // 4
console.log(makePower3(2)); // 8
console.log(makePower4(2)); // 16
// 函数改造
function makePowerTransform(power) {
return function (num) {
return Math.pow(num, power);
};
}
// 将指数固定 ,得到返回的函数,最后求幂
let makePower2TF = makePowerTransform(2);
let makePower3TF = makePowerTransform(3);
console.log(makePower2TF(2)); // 4
console.log(makePower2TF(3)); // 9
console.log(makePower2TF(4)); // 16
console.log(makePower3TF(4)); // 64
纯函数
- 纯函数概念
相同的输入
永远会得到相同的输出
.(没有
任何可观察的副作用
)- 纯函数类似于数学中的函数(用来描述输入和输出之间的关系)
y = f(x)
原生语法中的纯函数和非纯函数
slice , splice 分别是纯函数和不纯函数.
- slice 返回数组中的指定部分,不改变原数组
- splice 返回数组中的指定部分,改变原数组
案例解释 (相同的输入永远会得到相同的输出)
let arr = [1, 2, 3, 4, 5];
// slice: 调用三次 ,每次的输入 和 输出都一样,所以 slice 是纯函数
console.log(arr.slice(0, 3)); // [1,2,3]
console.log(arr.slice(0, 3)); // [1,2,3]
console.log(arr.slice(0, 3)); // [1,2,3]
// splice: 调用三次,每次的输入都一样,但是输出都不一样, 所以splice 不是纯函数
console.log(arr.splice(0, 3)); // [1,2,3]
console.log(arr.splice(0, 3)); // [4,5]
console.log(arr.splice(0, 3)); // []
// 封装一个纯函数
function getNum(n1, n2) {
return n1 + n2;
}
// 每次调用的输入和输出都一样,所以是纯函数
console.log(getNum(1, 2)); // 3
console.log(getNum(1, 2)); // 3
console.log(getNum(1, 2)); // 3
- 副作用
相关概念
- 副作用让一个函数变的不纯(如下例),如果函数
依赖于外部的状态
就无法保证输出相同,就会带来副作用.- 副作用来源: 配置文件,数据库,获取用户的输入
- 所有的外部交互都有可能带来副作用,副作用会使方法的通用性下降不适合拓展和可重用性.
副作用不可能完全禁止,尽可能控制他们在可控范围内发生
案例解释(副作用)
// 不属于纯函数, 当全局变量发生改变的时候,就会影响该函数的返回结果
let mini = 18;
function checkAge(age) {
return age >= mini;
}
console.log(checkAge(18)); // true
console.log(checkAge(20)); // true
// 纯函数改造 (将全局变量变为内部变量)
function checkAgeWithTransform(age) {
let mini = 18; // 此行属于硬编码(写死),后续可以通过函数柯里化进行优化
return age >= mini;
}
console.log(checkAge(18)); // true
console.log(checkAge(20)); // true
- 纯函数的优势(好处)
- 可以缓存(因为纯函数
相同的输入永远会得到相同的输出
的特点,可以把纯函数的结果缓存起来.)- 可测试(纯函数让测试更方便)
- 并行处理
- 在多线程环境下并行操作共享的内存数据很可能会出现以外情况
- 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数.(web worker ,可以新开一个线程)
案例解释 (可缓存)
- 案例一: 引入 lodash 库, 使用 memoize 函数, 缓存纯函数的计算结果
const _ = require("lodash");
// 记忆函数 封装一个计算圆形面积的函数
function getArea(r) {
console.log(r); // 3,4 (因为使用 memoize 记忆函数,所以只打印两次,后续都是从缓存中读取结果(面积))
return Math.PI * r * r;
}
const getAreaWithMemoize = _.memoize(getArea);
console.log(getAreaWithMemoize(3)); // 28.274333882308138
console.log(getAreaWithMemoize(3)); // 28.274333882308138
console.log(getAreaWithMemoize(3)); // 28.274333882308138
console.log(getAreaWithMemoize(4)); // 50.26548245743669
案例解释(可缓存)
- 案例二: 模拟 lodash 中的 memoize 函数
// 记忆函数 封装一个计算圆形面积的函数
function getArea(r) {
console.log(r); // 3,4 (因为使用 memoize 记忆函数,所以只打印两次,后续都是从缓存中读取结果(面积))
return Math.PI * r * r;
}
// 模拟 memoize 记忆函数
function memoize(fn) {
let cache = {};
return function () {
let key = JSON.stringify(arguments);
// console.log(arguments);
// console.log(JSON.stringify(arguments));
cache[key] = cache[key] || fn.apply(this, arguments);
// console.log(cache);
return cache[key];
};
}
let getAreaWithMemoize = memoize(getArea);
console.log(getAreaWithMemoize(3)); // 28.274333882308138
console.log(getAreaWithMemoize(3)); // 28.274333882308138
console.log(getAreaWithMemoize(3)); // 28.274333882308138
console.log(getAreaWithMemoize(4)); // 50.26548245743669
柯里化 (Haskell Brooks Curry)
- 基础概念
简单来说,函数柯里化就是 将接收多个参数的函数,转化为接收单一参数函数 的一种方法
案例解释(单一参数)
add(1)(2)(3); //6
add(1, 2, 3)(4); //10
add(1)(2)(3)(4)(5); //15
function add() {
// let args = Array.prototype.slice.call(arguments)
// let args = Array.of(...arguments)
let args = [...arguments];
let inner = function () {
args.push(...arguments);
return inner;
};
inner.toString = function () {
return args.reduce((pre, num) => pre + num);
};
return inner;
}
console.log(add(1, 2, 3, 4)(5).toString());
- 作用:
使用柯里化,可以解决代码中的
硬编码问题
案例解释(解决硬编码)
// 纯函数改造 (将全局变量变为内部变量)
function checkAgeWithTransform(age) {
let mini = 18; // 硬编码
return age >= mini;
}
console.log(checkAgeWithTransform(18)); // true
console.log(checkAgeWithTransform(20)); // true
// 改造一: 将 mini 当作参数,传入函数 (缺点:如果基准值长期不变的话,会存在传参重复的问题)
function checkAge(mini, age) {
return mini < age;
}
console.log(checkAge(18, 20)); // true
console.log(checkAge(18, 24)); // true
console.log(checkAge(18, 26)); // true
console.log(checkAge(26, 26)); // false
// 改造二:使用函数柯里化
function checkAgeWithCurry(mini) {
return function (age) {
return mini < age;
};
}
let checkAgeWithCurry18 = checkAgeWithCurry(18);
let checkAgeWithCurry26 = checkAgeWithCurry(26);
console.log(checkAge(18, 20)); // true
console.log(checkAge(18, 24)); // true
console.log(checkAge(18, 26)); // true
console.log(checkAge(26, 26)); // false
- lodash 库中的 curry 函数
优点: 可以先传入一部分参数,然后返回一个函数,再传入剩余的参数,最终得到结果
案例解释(lodash curry 基础使用)
const _ = require("lodash");
// lodash 柯里化使用
function getNum(n1, n2, n3) {
return n1 + n2 + n3;
}
const getNumWithCurry = _.curry(getNum);
// 可替换下面代码
// const getNumWithCurry = _.curry(function (n1, n2, n3) {
// return n1 + n2 + n3;
// });
console.log(getNumWithCurry(1, 2, 3)); // 6
console.log(getNumWithCurry(1)(2, 3)); // 6
console.log(getNumWithCurry(1, 2)(3)); // 6
匹配空白字符串
- 案例实践(使用
基础柯里化
封装)
// 柯里化案例
function match(reg) {
return function (str) {
return str.match(reg);
};
}
// 先传入部分参数
let findSpace = match(/\s/);
// 再传入剩余参数
console.log(findSpace("hello world")); // ['']
console.log(findSpace("helloworld")); // null
匹配空白字符串
- 案例实践(使用
lodash curry
函数封装)
const _ = require("lodash");
const matchWithCurry = _.curry(function (reg, str) {
return str.match(reg);
});
// 直接传入方法所需的全部参数
console.log(matchWithCurry(/\s/g, "hello world")); // [' ']
console.log(matchWithCurry(/\s/g, "helloworld")); // null
// 使用柯里化 传入部分参数,再传入剩余参数
let haveSpace = matchWithCurry(/\s/g);
console.log(haveSpace("hello world")); // [' ']
console.log(haveSpace("helloworld")); // null
过滤数组中符合条件的元素
- 案例实践(使用
lodash curry
函数封装))
const matchWithCurry = _.curry(function (reg, str) {
return str.match(reg);
});
// 使用柯里化 传入部分参数,再传入剩余参数
let haveSpace = matchWithCurry(/\s/g);
const filter = _.curry(function (fn, arr) {
return arr.filter(fn);
});
// 一次性传入函数所需全部参数
console.log(filter(haveSpace, ["hellow work", "coco", "jack yang"])); // [ 'hellow work', 'jack yang' ]
// 使用柯里化,先传入部分参数,再传入剩余参数
const filterSpace = filter(haveSpace);
console.log(filterSpace(["hellow work", "coco", "jack yang"])); // [ 'hellow work', 'jack yang' ]
- 模拟 lodash 库中的 curry 函数
// lodash 柯里化使用
function getNum(n1, n2, n3) {
return n1 + n2 + n3;
}
function curry(fn) {
console.log(arguments);
return function curried(...args) {
console.log(args);
console.log(arguments);
if (args.length < fn.length) {
return function () {
return curried(...args.concat(Array.from(arguments)));
};
}
console.log(args);
return fn(...args);
};
}
const getNumOrigin = curry(getNum);
console.log(getNumOrigin(1, 2)(3));
console.log(getNumOrigin(1, 2, 3));
console.log(getNumOrigin(1)(2, 3));
函数组合相关
概念:
函数组合:
- 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数就像是数据的管道,函数组合就是把这些管道连接在一起,让数据穿过多个管道形成最终结果.
- 函数组合默认
从右到左执行
管道:
- fn(g(h(x))) , 洋葱函数就好比一个管道,每嵌套一层,管道就变长.
- fn(g,h), 将
长管道分段成多个短管道
,每个短管道只负责一个功能.出问题只需要找到对应函数即可.
函数组合的特点:
- 纯函数和柯里化很容易写出洋葱代码,例如:
f(g(h(x)))
- 多函数嵌套时,如果代码出现问题则很难排查问题.
- 函数组合可以让我们把细粒度的函数重新组合成一个新的函数.
案例解释:
function compose(f, g) {
return function (value) {
return f(g(value));
};
}
function reverse(value) {
return value.reverse();
}
function first(value) {
return value[0];
}
const last = compose(first, reverse);
console.log(last([1, 2, 3, 4])); // 4
lodash 库中组合函数
的方法
- _flow() : 函数执行顺序,从左到右
- _flowRight() : 函数执行顺序,从右到左
const _ = require("lodash");
function reverse(value) {
return value.reverse();
}
function first(value) {
return value[0];
}
function toUpper(value) {
return value.toUpperCase();
}
// 从右往左执行
const fr = _.flowRight(toUpper, first, reverse);
console.log(fr(["one", "two", "three"])); // 'THREE'
// 从左往右执行
const f = _.flow([reverse, first, toUpper]);
console.log(f(["three", "two", "one"])); // 'ONE'
console.log(f(["four", "five", "six"])); // 'SIX'
模拟 lodash 库中的组合函数
function reverse(value) {
return value.reverse();
}
function first(value) {
return value[0];
}
function toUpper(value) {
return value.toUpperCase();
}
// 模拟 lodash 组合函数方法
function compose(...args) {
return function (value) {
// 深拷贝,避免影响原数组
let reverseData = [...args];
return reverseData.reverse().reduce((result, fn) => {
return fn(result);
}, value);
};
}
const f1 = compose(toUpper, first, reverse);
console.log(f1(["three", "two", "one"])); // 'ONE'
console.log(f1(["four", "five", "six"])); // 'SIX'
结合律
函数组合要满足结合律.
即: fn(f1,f2,f3) === fn(f1,fn(f2,f3)) === fn(fn(f1,f2),f3) 成立
案例解释(结合律)
const f1 = compose(compose(toUpper, first), reverse);
console.log(f1(["three", "two", "one"])); // 'ONE'
console.log(f1(["four", "five", "six"])); // 'SIX'
const f2 = compose(toUpper, compose(first, reverse));
console.log(f2(["seven", "eight", "nine"])); // 'NINE'
函数组合 调试案例
将字符串
NEVER SAY DIE
=>never-say-die
// lodash 组合函数 调试方法案例
// NEVER SAY DIE => never-say-die
const _ = require("lodash");
const split = _.curry((sep, str) => _.split(str, sep));
const join = _.curry((sep, arr) => _.join(arr, sep));
const f = _.flowRight(split(" "), _.toLower, join("-"));
console.log(f("NEVER SAY DIE")); // [ 'n-e-v-e-r-', '-s-a-y-', '-d-i-e' ]
// 声明调试方法
const log = (reslut) => {
console.log(reslut); // 'never,say,die'
return reslut;
};
const f1 = _.flowRight(join("-"), log, _.toLower, split(" "));
console.log(f1("NEVER SAY DIE"));
// 定位到问题后,重写 map 方法
const map = _.curry((fn, array) => _.map(array, fn));
const f2 = _.flowRight(join("-"), log, map(_.toLower), split(" "));
console.log(f2("NEVER SAY DIE")); // 'never-say-die'
// 优化调试方法
const trace = _.curry((tag, result) => {
console.log(tag, result); // [ 'map 之前', [ 'NEVER', 'SAY', 'DIE' ] ] , [ 'map 之后', [ 'never', 'say', 'die' ] ]
return result;
});
const f3 = _.flowRight(
join("-"),
trace("map 之后"),
map(_.toLower),
trace("map 之前"),
split(" ")
);
console.log(f3("NEVER SAY DIE")); // 'never-say-die'
lodash 中的 fp 模块
- fp 模块 提供了实用的对函数式编程友好的方法
- 提供了不可变的 auto-curried iteratee-first data-last 方法 (自动柯里化,函数优先,数据滞后)
案例解释
const _ = require("lodash");
const fp = require("lodash/fp");
// lodash 模块 (数据优先,函数滞后)
let r1 = _.map(["a", "b", "c"], _.toUpper);
console.log(r1); // 输出 ['A','B','C']
let r2 = _.split("hello world", " ");
console.log(r2); // 输出 ['hello','world']
// fp 模块 (函数优先,数据滞后)
let r3 = fp.map(fp.toUpper, ["a", "b", "c"]);
console.log(r3); // 输出 ['A','B','C']
let r4 = fp.split(" ", "hello world");
console.log(r4); // 输出 ['hello','world']
lodash 中 map 方法 与 fp 模块 map 方法的区别
两个 map 方法的入参数量有所不同.
const _ = require("lodash");
const fp = require("lodash/fp");
let r1 = _.map(["1", "2", "3"], parseInt);
console.log(r1); // [ 1, NaN, NaN ]
let r2 = fp.map(parseInt, ["1", "2", "3"]);
console.log(r2); // [ 1, 2, 3 ]
point free 模式
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
案例解释:
const fp = require("lodash/fp");
const f = fp.flowRight(fp.replace(/\s+/g, "_"), fp.toLower);
console.log(f("Hello World")); // 'hello_world'
案例二:
const fp = require("lodash/fp");
// 将 world work web ==> W. W. W
const firstLetterToUpper = fp.flow(
fp.split(" "),
fp.map(fp.toUpper),
fp.map(fp.first),
fp.join(". ")
);
console.log(firstLetterToUpper("world work web")); // W. W. W
// 代码优化( 因为 map 调用了两次,所以可以优化)
const firstLetterToUpperOptimize = fp.flow(
fp.split(" "),
fp.map(fp.flow(fp.toUpper, fp.first)),
fp.join(". ")
);
console.log(firstLetterToUpperOptimize("world work web")); // W. W. W
Functor 函子
- 什么是函子
- 容器: 包含值和值的变形关系(这个变形关系就是函数)
- 函子: 是一个特殊的容器,通过一个普通的对象实现, 该对象具有 map 方法, map 方法可以运行一个函数对值进行处理(变形关系)
- 函子的作用
- 可将函数式编程过程中的副作用控制在可控范围内
- 可进行 异常处理,异步操作等等
- 总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了 map 契约的对象
- 可以把函子想象成一个盒子,这个盒子里封装了一个值
- 如果想要处理盒子中的值,需要给盒子的 map 方法传递一个
处理值的函数(纯函数)
,由这个函数对值进行处理- 最终 map 方法返回一个包含新值额盒子(函子)
案例解释:
// Functor 函子
class container {
constructor(value) {
this._value = value;
}
map(fn) {
return new container(fn(this._value));
}
}
let r1 = new container(10).map((v) => v + 2).map((v) => v * 2);
console.log(r1); // container { _value: 24 }
// 代码优化 (解决 重复使用 new 关键字去声明)
class containerOptimize {
static of(value) {
return new containerOptimize(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return new container(fn(this._value));
}
}
let r2 = containerOptimize
.of(10)
.map((v) => v + 2)
.map((v) => v * 2);
console.log(r2); // container { _value: 24 }
MayBe 函子
- 作用: 用于处理可能为空的值
- 缺点: 因为不会报错,不会影响下面代码执行,所以没办法判断是哪一步 出现的空值.
案例解释:
// MayBe 函子
class MayBe {
static of(value) {
return new MayBe(value);
}
constructor(value) {
this.value = value;
}
map(fn) {
console.log(this.isNothing());
return this.isNothing() ? MayBe.of(this.value) : MayBe.of(fn(this.value));
}
// 声明一个方法,用于判断是否为空
isNothing() {
return this.value === null || this.value === undefined;
}
}
console.log(MayBe.of("hello World").map((v) => v.toUpperCase())); // 输出 HELLO WORLD (此时没有 判空操作)
// console.log(MayBe.of(null).map((v) => v.toUpperCase())); // 会报错 (此时没有 判空操作)
console.log(MayBe.of(undefined).map((v) => v.toUpperCase())); // MayBe { value: null } (添加 isNothing 判空方法后)
console.log(
MayBe.of("hello World")
.map((v) => v.toUpperCase())
.map((v) => null)
.map((v) => v.split(""))
); // MayBe { value: null } (如果多层调用后,则不好判断是哪个几点出现的空值,)
Either 函子
- Either 来给你这中的任何一个,类似于 if else 的处理方式
- 异常会让函数变的不纯,Eiterh 函子可以用来做异常处理
案例解释:
// Either 函子
class Left {
static of(value) {
return new Left(value);
}
constructor(value) {
this.value = value;
}
map(fn) {
return this;
}
}
class Right {
static of(value) {
return new Right(value);
}
constructor(value) {
this.value = value;
}
map(fn) {
return Right.of(fn(this.value));
}
}
let r1 = Left.of(12).map((num) => num + 2);
let r2 = Right.of(12).map((num) => num + 2);
console.log(r1); // Left { value: 12 }
console.log(r2); // Right { value: 14 }
function parseJson(str) {
try {
return Right.of(JSON.parse(str));
} catch (error) {
return Left.of({ error: error.message });
}
}
// 传入错误的 json 字符串
let r3 = parseJson("{name: iike}");
console.log(r3); // Left { value: { error: 'Unexpected token a in JSON at position 1' } }
// 传入正确的 json 字符串
let r4 = parseJson('{"name": "iike"}');
console.log(r4); // Right { value: { name: 'iike' } }
let r5 = parseJson('{"name": "iike"}').map((v) => v.name.toUpperCase());
console.log(r5); // Right { value: 'IIKE' }
IO 函子
- IO 函子中的 _value 是一个函数,这里是吧函数作为值来处理
- IO 函子可以把不纯的动作存储在 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作
- 把不纯的操作交给调用者来处理
案例解释:
const fp = require("lodash/fp");
// IO 函子
class IO {
static of(value) {
return new IO(function () {
return value;
});
}
constructor(fn) {
this.value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this.value));
}
}
// process 为 node 环境下的一个对象.
const r1 = IO.of(process).map((v) => v.execPath);
console.log(r1); // IO { value: λ(函数) }
console.log(r1.value()); // 'C:\Program Files\nodejs\node.exe'
const r2 = new IO(() => process).map((v) => v.execPath);
console.log(r2.value()); // 'C:\Program Files\nodejs\node.exe'
IO 函子的问题
IO 函子可能出现 函数嵌套的问题
const fs = require("fs");
const fp = require("lodash/fp");
// IO 函子
class IO {
static of(value) {
return new IO(function () {
return value;
});
}
constructor(fn) {
this.value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this.value));
}
}
const readfile = (path) => {
return new IO(function () {
return fs.readFileSync(path, "utf-8");
});
};
const print = (x) => {
return new IO(function () {
console.log(x);
return x;
});
};
let cat = fp.flowRight(print, readfile);
console.log(cat("package.json")); // IO { value: λ }
console.log(cat("package.json").value()); // IO { value: λ }
console.log(cat("package.json").value().value()); // package.json 文件中的内容
Task 函子
解决异步处理的问题
案例: 读取 package.json 文件,并获取版本号(version)
// 读取 package.json 文件,并获取版本号(version)
const { task } = require("folktale/concurrency/task");
const fs = require("fs");
const { split, find } = require("lodash/fp");
function readFile(fileName) {
return task((resolver) => {
fs.readFile(fileName, "utf-8", (err, data) => {
if (err) {
resolver.reject(err);
} else {
resolver.resolve(data);
}
});
});
}
readFile("package.json")
.map(split("\n"))
.map(find((x) => x.includes("version")))
.run()
.listen({
onResolved(data) {
console.log(data); // ' "version": "0.1.0",'
},
onRejected(error) {
console.log(error);
},
});
Pointed 函子
Pointed 函子是实现了 of 静态方法的函子.
of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Contest (把值放到容器中,使用 map 来处理值)
Monad (单子)函子
Monad 函子是可以变扁的 Pointed 函子, IO(IO(x))
一个函子如果具有 join 和 of 两个方法并遵守一些定律的就是一个 Monad
const fs = require("fs");
const fp = require("lodash/fp");
// IO 函子
class IO {
static of(value) {
return new IO(function () {
return value;
});
}
constructor(fn) {
this.value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this.value));
}
join() {
return this.value();
}
flatMap(fn) {
return this.map(fn).join();
}
}
const readfile = (path) => {
return new IO(function () {
return fs.readFileSync(path, "utf-8");
});
};
const print = (x) => {
return new IO(function () {
console.log(x);
return x;
});
};
let cat = fp.flowRight(print, readfile);
console.log(cat("package.json")); // IO { value: λ }
console.log(cat("package.json").value()); // IO { value: λ }
console.log(cat("package.json").value().value()); // package.json 文件中的内容
// 优化后的代码
let r = readfile("package.json").flatMap(print).join();
console.log(r); // package.json 文件中的内容
let r2 = readfile("package.json")
.map((v) => v.toUpperCase())
.flatMap(print)
.join();
console.log(r2); // 文件内容全部大写
FolkTale 工具库
folktale 一个标准的函数式编程库
- 和 lodash , ramda 不同的是, folktale 没有提供很多功能函数
- 只提供了一些函数式处理的操作,例如: compose,curry 等, 一些函子 Task ,Eiter , MayBe 等
folktale 基本使用:
const { compose, curry } = require("folktale/core/lambda");
const { toUpper, first } = require("lodash");
// folktale 中的curry 函数,第一个参数 是为了避免某些报错
const f = curry(2, (x, y) => x + y);
console.log(f(1, 2)); // 2
console.log(f(1)(2)); // 2
const f2 = compose(toUpper, first);
console.log(f2(["one", "two"])); // ONE