共计 5759 个字符,预计需要花费 15 分钟才能阅读完成。
前言
在讲函数式组件之前,笔者有必要申明一下,笔者对函数式编程知之甚少,此篇文章全是因为函数式编程在当今前端开发较为风行,所以才要去学习 Orz。像 ES6 中的箭头函数,Redux 中的 compose,React 16.6 之后的 React.memo(),16.8 之后的 Hooks,应用函数式组件成为支流(天然,Vue3 中也有 composition API 等)
因为咱们应用的各种框架,越来越偏差于函数式开发,所以为了看懂、应用这些代码,咱们有必要学习函数式编程
什么是函数式编程
写代码时,不同的写法造成不同的门派,支流的编程范式有两种,即命令式编程(Imperative programming)和申明式编程(Declarative programming)
当然还有像事件驱动编程等,此处不讲
在命令式编程语言中个别反对四种根本语句:
- 运算语句
- 循环语句(for、while)
- 条件分支语句(if else、switch)
- 无条件分支语句(return、break、continue)
它关注的过程,所以也被称为过程化编程,与之对应的是申明式编程
- 命令式编程通知计算机如何计算、关怀解决问题的步骤
- 申明式编程通知计算机须要计算什么,关怀解决问题的指标
两种编程形式在 JavaScript 均有开花,在以往的开发中(即 React、Vue 等未呈现前),咱们习惯应用命令式编程,其中的代表就是面向对象编程,即通过对象、类、继承、封装等个性实现面向对象编程,风行的库是 jquery
即便当初不那么风行,但 JavaScript 是始终反对申明式编程的,即通过函数式编程来实现,其起因是函数是一等公民,它能作为参数和返回值。风行的库是 underscorejs、lodash
函数式编程有哪些特点
柯里化(Currying)、缓存函数、纯函数、函数组合(compose)、不可变数据、高阶函数
咱们顺次讲讲这几个特点
柯里化
什么是柯里化
柯里化是把承受多个参数的函数变换成承受一个繁多参数(最后函数的第一个参数)的函数,并且返回承受余下的参数而且返回后果的新函数的技术
——wiki
也就是说,柯里化是一种函数的转换,它是将一个函数从 f(a, b, c)
的写法转换为 f(a)(b)(c)
艰深来讲:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。它不会调用函数,只是对函数进行转换
// 柯里化之前
function add(x, y) {return x + y;}
add(1, 2); // 3
// 柯里化之后
function add(y) {return function (x) {return x + y;};
}
// ES6 箭头函数示意
const add = x => y => x + y;
add(2)(1);
如上案例,咱们就是将 add(1, 2)
转换为了 add(2)(1)
,当然,此案例是模仿柯里化,一般来说,咱们会写个「转换函数」将函数适配成柯里化型函数
const curryAdd = curry(add)
console.log(curryAdd(1)(2)) // 3
那么如何实现 curry 呢?须要满足两点
- 判断以后函数传入的参数是否大于或等于
fn
须要参数的数量,如果是,间接执行fn
- 如果传入参数数量不够,返回一个闭包,暂存传入的参数,并从新返回
curry
函数
const curry = (fn, ...args) => {if (args.length >= fn.length) {return fn(...args)
} else {return (...args2) => curry(fn, ...args, ...args2)
}
}
咱们来一个简略的实例验证一下:
function fun(a, b, c) {return a + b + c;}
const curryFun = curry(fun);
curryingFun(1)(2)(3); // 6
curryingFun(1, 2)(4); // 7
curryingFun(1, 2, 5); // 8
翻看函数式编程概念的两个库:undersore.js 及 lodash 中各自实现的 curry
- undersore.js
- lodash
箭头函数的应用
在下面的例子中,咱们频繁应用箭头函数,箭头函数的魅力之一是它合乎函数式编程的思维,例如:
function add(y) {return function (x) {return x + y;};
}
const add = x => y => x + y;
// redux 中的 compose 函数
const compose = (...func) => {if (func.length === 0) {return args => args}
if (func.length === 1) {return func[0]
}
return func.reduce((a,b) => (...args) => a(b(...args)))
}
function enhancer(originF) {return function (...args) {console.log('before');
const result = originF(...args);
console.log('after');
return result;
};
}
// 等于
const enhancer =
(originF) =>
(...args) => {console.log('before');
const result = originF(...args);
console.log('after');
return result;
};
// 三数之乘
const multi = a => b => c => a * b * c
笔者认为,柯里化就是利用了闭包的个性,将参数存着,到了要应用的时候再开释,而这正式柯里化的利用之一
柯里化的利用
在开发中,咱们会遇到这种状况,开发了一个工具函数,在调用时,会应用一个频繁的参数,而如果将这个参数提取进去,就能优化代码,如
function add(a, b) {return a + b}
add(10, 1) // 11
add(10, 11) // 21
add(10, 111) // 121
如果咱们柯里化,将第一个参数 10 内置进函数中,代码就能清晰明了
// 最简略的优化
functon add10(b) {return add(10, b)
}
add10(1) // 11
add10(11) // 21
add10(111) // 121
函数 add10
就是通过了柯里化的函数,就像咱们上文所讲,个别用个包装函数将其适配成柯里化型函数,这里咱们调用上文的 curry 函数
// 完整版
function add(a, b) {return a + b;}
// 柯里化包装函数
const curry = (fn, ...args) => {if (args.length >= fn.length) {return fn(...args)
} else {return (...args2) => curry(fn, ...args, ...args2)
}
}
const add10 = curry(add, 10)
add10(1) // 11
add10(11) // 21
add10(111) // 121
缓存函数
缓存函数就是将函数返回值缓存起来的办法。与柯里化相比,它仿佛更简略。例如
function add(a, b) {return a + b;}
let memoAdd = memoize(add)
memoAdd(1, 2) // 3
memoAdd(1, 3) // 雷同的参数,第二次调用时,从缓存中取出数据
如何实现呢?
其实很简略,就是用闭包存在内存中。如果统一,就返回,不统一就存在对象中,再返回
const memoize = (func) => {let cache = {}
return function(key) {if (!cache[key]) {cache[key] = func.apply(this, arguments)
}
return cache[key]
}
}
纯函数
笔者认为纯函数就相似数学中的函数,例如 y = f(x)
,更具体点就是:y=2x
,雷同的输出,就有雷同的输入,在执行过程中不会产生副作用
所有它有两个特点:
- 不扭转原数据(没有副作用)
- 返回一个新数据
const arr = [1, 2, 3]
// 纯函数
arr.slice(1)
console.log(arr) // [1, 2, 3]
// 产生副作用,非纯函数
arr.splice(1)
console.log(arr) // [1]
JavaScript 中的哪些 API 是纯函数
数组:slice、concat、map、reduce、filter
slice、concat 咱们在 拷贝的机密 已经讲过,是浅拷贝的办法
map、filter 是 ES6 新增的 API
字符串:String 类型及其他的根本类型的 API 都是不可变的,它们的每个 API 都是纯函数,不会扭转原数据
var str = "hello"
console.log(str.slice(0, 3)) // hel , slice 在 String 类型中同样实用,console.log(str) // hello
console.log(str.substr(0, 3)) // hel
console.log(str) // hello
console.log(str.substring(0,3)); // hel
console.log(str) // hello
// 其余的 API 也是如此
函数组合(compose)
就是将多个函数顺次执行,将这些函数组合起来,主动顺次执行,这一过程就是函数组合
这么做的目标是为了让代码更加简略且具备可读性
function double(num) {return num * 2}
function square(num) {return num ** 2}
// 一般做法
const res = square(double(10))
console.log(res) // 400
// 函数组合
function composeFn(fn1, fn2) {return function (count) {return fn2(fn1(count))
}
}
const newFn = composeFn(double, square)
const res2 = newFn(20) // 1600
看看 underscore 的 compose 函数
function compose() {
var args = arguments;
var start = args.length - 1;
return function() {
var i = start;
var result = args[start].apply(this, arguments);
while(i--) result = args[i].call(this, result);
return result;
}
}
看到 compose 是否很相熟,redux 中的源码是这样写的
function compose(...funcs) {if (funcs.length === 0) {return arg => arg}
if(funcs.length === 1) {return funcs[0]
}
return funcs.reduce((a,b) => (...args) => a(b(...args)))
}
如果在理论开发中,就能够用这一概念解决洋葱代码
fn3(fn2(fn1(fn0(x)))) // 洋葱代码
var result = compose(f3, f2, f1, f0) // 函数组合代码
result(x)
高阶函数
简略来说,高阶函数就是一个返回另一个函数的函数
function sayHello() {return function() {console.log("hello")
}
}
其本质是因为函数是一等公民,满足作为参数或者返回值
其 React 中也有高阶组件的概念,其原理都一样,行将组件当作参数传入,返回另一个组件
不可变数据
不可变数据结构式函数式编程中的基本准则
JavaScript 的对象是可变的,起因是为了节约内存,但也同样带来了副作用,当应用程序简单后,保护代码难度会加深
咱们在 拷贝的机密 中已经讲过,根本类型的数据是不可变的,因为它们存在栈内存,而援用类型(即对象)的数据是可变的,它存在堆内存,当赋值时,拷贝的时它的援用地址
所以咱们在赋值时会依据状况应用浅拷贝、深拷贝来实现
在 React 开发时,咱们会用 immutable.js、immer 对数据进行不可变
总结
咱们不论其余的,单单就函数式编程在前端方面有哪些用处?如在 Koa 中的洋葱模型、redux 中的 compose 函数就是函数组合,React 开发时的 HOC 高阶组件就采纳了高阶函数,以及它(React)为了让数据放弃唯一性,而能够援用 不可变数据库(immutable.js、immer)是函数式编程的个性——不可变数据
当然,这些只是笔者对函数式编程肤浅的认知,其内容当然不知以上说的几点,还有惰性函数、函子等知识点,以及其余语言中的函数式编程,待笔者等级晋升后再来战
参考资料
- 扼要 JavaScript 函数式编程 - 入门篇
- 函数式编程思维
- 浅析 JavaScript 函数式编程
- 函数式编程(FP)
- 前端开发 js 函数式编程实在用处体现在哪里?
- JavaScript 专题之函数组合
系列文章
- 深刻了解 JavaScript——开篇
- 深刻了解 JavaScript——JavaScript 是什么
- 深刻了解 JavaScript——JavaScript 由什么组成
- 深刻了解 JavaScript——所有皆对象
- 深刻了解 JavaScript——Object(对象)
- 深刻了解 JavaScript——new 做了什么
- 深刻了解 JavaScript——Object.create
- 深刻了解 JavaScript——拷贝的机密
- 深刻了解 JavaScript——原型
- 深刻了解 JavaScript——继承
- 深刻了解 JavaScript——JavaScript 中的始皇
- 深刻了解 JavaScript——instanceof——找祖籍
- 深刻了解 JavaScript——Function
- 深刻了解 JavaScript——作用域
- 深刻了解 JavaScript——this 关键字
- 深刻了解 JavaScript——call、apply、bind 三大将
- 深刻了解 JavaScript——立刻执行函数(IIFE)
- 深刻了解 JavaScript——词法环境
- 深刻了解 JavaScript——执行上下文与调用栈
- 深刻了解 JavaScript——作用域 VS 执行上下文
- 深刻了解 JavaScript——闭包
- 深刻了解 JavaScript——防抖与节流