Function 函数的使用方法 -- Javascript

Function 相关知识

apply,bind,call 作用及区别

三者的区别

  1. callapply 改变了函数的 this 上下文后便执行该函数 , bind 则是返回改变了上下文后的一个函数
  2. callbind 都可以接收多个参数, apply 接收两个参数 ,第二个参数为数组
  3. callapply 都是立即执行, bind 是返回一个函数(需要手动调用)

参考文章@micherwa

参考视频@技术蛋老师

call (可接收若干参数)

  1. 调用 call 的对象,必须是个函数 Function.
  2. call 的第一个参数是一个对象,Function 的调用者,将会指向这个对象.如果不传,则默认为全局对象 window.
  3. 第二个参数开始,可以接受任意个参数. 每个参数会映射到相应位置的 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 (可接收两个参数)

  1. 它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
  2. 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 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 (手动调用)

  1. 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 函数式编程

函数是一等公民

  1. 函数可以作为参数传递
  2. 函数可以作为返回值
  3. 函数可以作为变量赋值
  • 一等公民指的是函数与其他数据类型一样,处于平等地位,能参与运算,也可以作为参数传递,赋值等。
  • 代码优化案例
// 优化前
let functionInvoke = {
  index(posts) {
    return views.index(posts);
  },
  show(pic) {
    return views.show(pic);
  },
};

// 优化后

let functionInvoke = {
  index: views.index,
  show: views.show,
};

高阶函数

  1. 接受一个或者多个函数作为其入参
  2. 返回值是一个函数
  • 两者满足其一,则为高阶函数

- 使用高阶函数的意义

  • 抽象可以帮我们屏蔽一些细节,只需要关注目标
  • 高阶函数是用来抽象通用的问题

- 函数作为参数案例

模拟常用的高阶函数

// 模拟常用高阶函数 (函数作为参数)

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); // '不执行 !!!'

闭包

传统概念:

闭包: 函数和其周围状态的引用捆绑在一起,形成闭包

通俗来讲:

  1. 一个可以访问另一个函数内部变量的函数就叫做闭包 (一个函数可以访问另一个函数内部的变量,这就叫闭包)
  2. 比如: 函数 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

纯函数

- 纯函数概念

  1. 相同的输入永远会得到相同的输出.(没有任何可观察的副作用)
  2. 纯函数类似于数学中的函数(用来描述输入和输出之间的关系) 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

- 副作用相关概念

  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

- 纯函数的优势(好处)

  1. 可以缓存(因为纯函数相同的输入永远会得到相同的输出的特点,可以把纯函数的结果缓存起来.)
  2. 可测试(纯函数让测试更方便)
  3. 并行处理
    • 在多线程环境下并行操作共享的内存数据很可能会出现以外情况
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数.(web worker ,可以新开一个线程)

案例解释 (可缓存)

  1. 案例一: 引入 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

案例解释(可缓存)

  1. 案例二: 模拟 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 模式

  1. 不需要指明处理的数据
  2. 只需要合成运算过程
  3. 需要定义一些辅助的基本运算函数

案例解释:

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 函子

  1. 什么是函子
  • 容器: 包含值和值的变形关系(这个变形关系就是函数)
  • 函子: 是一个特殊的容器,通过一个普通的对象实现, 该对象具有 map 方法, map 方法可以运行一个函数对值进行处理(变形关系)
  1. 函子的作用
  • 可将函数式编程过程中的副作用控制在可控范围内
  • 可进行 异常处理,异步操作等等
  1. 总结
  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现了 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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值