1-7、函数式编程

函数式编程:鼓励使用纯函数(Pure Functions),即对于相同的输入,始终产生相同的输出,并且没有副作用(没有改变外部状态的行为)

发展历程

命令式 => 面向对象 => 面向函数
面试题:
将数组:[‘process&%coding’, ‘object&%coding’, ‘function&%coding’]
转为 JSON:[{ name: ‘Process Coding’}, { name: ‘Object Coding’}, { name: ‘Function Coding’}]
命令式代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const arr = ["process&%coding", "object&%coding", "function&%coding"];
const parseArr = [];
const Parser = (arr, parseArr) => {
arr.map((item) => {
const newItem = [];
item.split("&%").map((_item) => {
newItem.push(_item[0].toUpperCase() + _item.slice(1));
});
parseArr.push({ name: newItem.join(" ") });
});
};

Parser(arr, parseArr);

// 存在的问题
// 1. 有包裹逻辑 - 需要看完整段代码才能明白是在做啥
// 2. 存在临时变量,并且首尾封闭 - 拓展/返回临时变量的难度更高

对象式代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Parser = {
toJson(arr, parseArr) => {
arr.map((item) => {
const newItem = [];
item.split("&%").map((_item) => {
newItem.push(_item[0].toUpperCase() + _item.slice(1));
});
parseArr.push({ name: newItem.join(" ") });
});
};
}

Parser.toJson(arr, parseArr);

函数式代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 需求拆解:
// 1. 数组转为 JSON:arrToJSON
// 2. arrToJSON = stringFormat + objHelper
// 3. stringFormat = split + capitalize + join

const arr = ["process&%coding", "object&%coding", "function&%coding"];

// 原子逻辑:
// objHelper: 生成对象
const createObj = (key, anyValue) => {
const obj = {
[key]: anyValue
}
return obj
}

// strSplit: 按需分割
const strSplit = (str, splitKey) => str.split(splitKey)

// capitalize: 首字母大写
const capitalize = str => {
return str[0].toUpperCase() + str.slice(1)
}

// 逻辑拼装:
// stringFormat: 字符串处理
const stringFormat = str => {
return strSplit(str, '&%').map(item => capitalize(item)).join(' ')
}

const objHelper = (str) => {
return createObj('name', str)
}

// arrToJSON: 生成 JSON
const arrToJSON = (arr) => {
return arr.map(item => objHelper(stringFormat(item)))
}

// 好处:
// 1、将复杂的逻辑通过合理的拆分,变成简单功能的拼装,利于后期的维护与扩展

函数式编程的原理、特点

什么是函数式编程?

将复杂功能“因式分解”,然后使用“加法结合律”组装完成功能的思维形式

理论思想

函数是一等公民
每个原子功能,就是一个采用命令式编程的函数,并且属于惰性执行(需要的时候才会执行)
惰性执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 非惰性
function createPersonNoLazy() {
console.log("[ createPersonNoLazy person is created ] >");
return {
name: "xx",
age: 30,
};
}
// 每次执行都重新生成新的
const preson1 = createPersonNoLazy();
const preson2 = createPersonNoLazy();

// 惰性
function createPersonLazy() {
console.log("[ createPersonLazy person is created ] >");
const person = {
name: "xx",
age: 30,
};

// 函数重写
createPersonLazy = () => {
console.log("[ createPersonLazy person is existed ] >");
return person;
};

return person;
}

// 第一次执行都生成新的,后续直接返回 person
const person3 = createPersonLazy();
const person4 = createPersonLazy();

函数式编程的要求

无状态

指函数在执行时不依赖或修改外部状态。它的行为仅由输入参数决定,并且对于相同的输入,总是产生相同的输出,不受外部环境的影响。

无副作用

指函数在执行过程中不对外部环境产生可观察的影响,即不会对输入的[参数、外部变量]进行修改。

函数式编程的实际开发

纯函数改造

满足无状态 && 无副作用的函数就是纯函数

1
2
3
4
5
6
7
8
9
10
11
const a = 1

// 引入了外部变量,违反了无状态
const add = x => a + x

const add = (a, x) => a + x // 无状态

// 改变了参数/外部变量,违反了无副作用
const add = obj => obj.x++

const add = obj => { ...obj, x: obj.x + 1} // 无副作用

函子

定义:一个类/构造函数,具有map方法,每次调用map会生成新的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Box = function(val) {
this.val = val;
}

// of 函数:让使用者不必使用 new 来新建对象
Box.of = function(val) {
return new Box(val);
}
Box.prototype.map = function(func) { // 必须
return this.isNull() ? Box.of(null) : Box.of(func(this.val));
}

// 错误处理-函子
Box.prototype.isNull = function() {
return this.val === null || this.val === undefined

}

const add = x => x + 1
const square = x => x * x
const setNull = () => null

Box.of(1).map(add).map(setNull).map(square).val === null // true

函子的作用:适合消除副作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Monad {
constructor(value) {
this.value = value;
}

bind(transform) {
// `bind` 方法用于将当前 Monad 的值传递给一个函数(transform),并返回一个新的 Monad
return transform(this.value);
}

// `value` 方法用于获取 Monad 的值
value() {
return this.value;
}
}

// 使用示例
const readFile = function (filename) {
const content = fs.readFileSync(filename, "utf-8");
return new Monad(content);
};

const print = function (x) {
console.log(x);
return new Monad(x);
};

const tail = function (x) {
const lastLine = x.split('\n').pop();
return new Monad(lastLine);
};

// 链式操作
const monad = readFile('./xxx.txt').bind(tail).bind(print);
// 执行操作
monad.value(); // 这里触发整个流程的执行

图解 Monad - 阮一峰的网络日志

加工 & 组装

加工 - 柯里化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 未柯里化
const add1 = (x, y, z) => x + y + z
add(1, 2, 3)

// 柯里化
const add2 = (x) => {
return y => {
return z => x + y + z
}
}
add2(1)(2)(3)

// 柯里化转换函数
const toKLH = (fn) => {
const KLH = (...arg) => {
if (arg.length === fn.length) {
return fn(...arg);
} else {
return (...arg2) => {
return KLH(...arg.concat(arg2));
};
}
};

return KLH;
};

// 分批次使用
const add10 = toKLH(add1)(10) // 计算初始值为 10 的加法
const add20 = toKLH(add1)(20) // 计算初始值为 20 的加法

add10(1)(3)
add20(1)(3)

为什么需要柯里化,为了函数的输入输出单值化(单元函数,更利于组合),更加方便操作多值函数

组装 - 高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const sum1 = x => x + 1
const sum2 = x => x + 2

// 函数式
const compose = (f, g) => {
return x => {
return f(g(x))
}
}
const sum1_2 = compose(sum1, sum2)(value) // 4

// 命令式
sum2(sum1(value))()

// 对象式
valueInstance.sum1().sum2()

补充知识

高阶函数

定义:函数为参的函数
黑话:逻辑外壳

toString()、valueOf()

当转为字符串时,先调用 toString()
若返回的是基本类型,则直接调用 String()
否则再调用 valueOf(),若返回的是基本类型,则再调用 String(),否则就报错Uncaught TypeError: Cannot convert object to primitive value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var obj={
"user":"张三",
"toString":function () {
console.log('1.执行了toString()方法');
return {};
},

"valueOf":function (){
console.log('2.执行了valueOf()方法');
return 12 // 基本类型-number
}
}

console.log(String(obj));
// 1.执行了toString()方法
// 2.执行了valueOf()方法
// '12'


var obj={
"user":"张三",
"toString":function () {
console.log('1.执行了toString()方法');
return {};
},

"valueOf":function (){
console.log('2.执行了valueOf()方法');
return {} // 复杂类型-object
}
}

console.log(String(obj));
// 1.执行了toString()方法
// 2.执行了valueOf()方法
// Uncaught TypeError: Cannot convert object to primitive value
// at String (<anonymous>)
// at <anonymous>:14:13

面试题

如何使用正确的遍历

数组:for、find、findIndex、forEach、map、filter、reduce、sort、some、every
对象:for in
类数组:for
可遍历:for、for of

为什么数组有这么多的遍历方法?

  • for:通用
  • find:找到某个值
  • findIndex:找到某个值的下标
  • forEach:遍历进行逻辑处理
  • map:生成新数组,顺带进行逻辑处理
  • filter:过滤满足条件的值,并生成新数组
  • reduce:累积
  • sort:排序
  • some:是否 >=1 个满足条件
  • every:是否所有满足条件

本质逻辑是:满足函数式编程,让每个函数有自己应该做的事情

JS 里面的副作用函数有哪些?

  • split:不会改变原数据
  • slice:不会改变原数据
  • splice:会改变原数据
  • pop:会改变原数据
  • push:会改变原数据
  • shift:会改变原数据
  • unshift:会改变原数据
  • reverse:会改变原数据
  • sort:会改变原数据

柯里化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 实现一个 add(1)(2)(3)...(n)() 的累加函数
const add = (...args1) => {
const inner = (...args2) => {
if (args2.length) {
args1.push(...args2);
return inner;
} else {
return args1.reduce((total, curr) => (total += curr), 0);
}
};

return inner;
}

add(1)() // 1
add(1)(2)() // 3
add(1)(2)(3)() // 6
add(1)(2)(3)(4)() // 10
add(1, 2, 3, 4)() // 10

柯里化与闭包的关系

“孪生子”
闭包定义:返回函数的函数,其中内部函数使用了外部函数定义的变量,形成了闭包
柯里化定义:将多参数的函数转为接受单/部分参数的函数,并且返回接受剩余参数和返回结果的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 闭包 ----
const add10 = () => {
const num = 10
return (x) => x + num
}
const addInit = add10()
addInit(10) // 10+10,20

// 柯里化 ----
// 初始函数 add
const add1 = (x, y) => {
return x + y
}
add1(10, 10)

// 初始函数 add 柯里化后
const add2 = (x) => {
return y => {
return x + y
}
}
add2(10)(10)

柯里化的运用

防抖、节流

防抖

定义:触发后,x 时间后才生效(每次触发都重新计时),适用于:onresize、输入框搜索等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const debounce = (fn, delay) => {
delay = delay || 200
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
}, delay)
}
}

let count = 0
window.onresize = debounce((e) => {
console.log('e.type', e.type)
console.log('count', ++count)
}, 500)

节流

定义:x 时间内只会触发一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 const throttle = (fn, delay) => {
delay = delay || 200;
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
};
};

let count = 0
window.onresize = debounce((e) => {
console.log('e.type', e.type)
console.log('count', ++count)
}, 1000)

缓存计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 需求:大数据的计算
const calculateFn = (num)=>{
const startTime = new Date()
for(let i=0;i<num;i++){
// 大数计算.....
}
const endTime = new Date()
console.log(endTime - startTime)
return "Calculate big numbers"
}

calculateFn(10_000_000_000) // 耗时 8s
calculateFn(10_000_000_000) // 每次调用都耗时 8s
calculateFn(10_000_000_000) // 每次调用都耗时 8s

// 柯里化-缓存改造
const caches = (fn) => {
const cacheResult = {};
return function (num) {
if (!cacheResult[num]) {
cacheResult[num] = fn(num);
}
return cacheResult[num];
};
};

const calculateCacesFn = caches(calculateFn);

calculateCacesFn(10_000_000); // 首次调用,耗时 8s
calculateCacesFn(10_000_000); // 重复调用,直接拿值
calculateCacesFn(20_000_000); // 首次调用,耗时 8s
calculateCacesFn(20_000_000); // 重复调用,直接拿值

实际业务

业务需求:某个函数调用 n 次后,不再调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const beforDo = (deNum, fn) => {
let count = 0
let result
return function() {
if(count < deNum) {
result = fn.apply(this, arguments)
count++
}
return result
}
}
const fn = beforDo(3, (x) => console.log(x))
fn(1) // 1
fn(2) // 2
fn(3) // 3
fn(4) // 函数将不再执行

1-7、函数式编程
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/1-7、函数式编程/
作者
黄智强
发布于
2024年1月13日
许可协议