函数式编程是一种编程模式。将代码抽象成各种各样的函数,并且将函数当做一等公民,能够作为其他函数的参数或者返回值传递。
JavaScript 既支持面对对象编程,也支持函数式编程。
面向对象编程是一种编程模式,将代码抽象成各种各样的对象。
面向对象的三大特性:
- 封装:将属性和方法封装到一个对象或者类中,就称之为封装。
- 继承:子类可以继承父类的属性和方法,可以减少重复的代码和逻辑。
- 多态:不同的数据类型进行同一个操作,表现出不同的行为。
function add(param1, param2) { return param1 + param2 } add(1, 2) add('Hello', 'World')
但是也有人认为 JS 中的这种表现不是面对对象语言中严格意义上的多态。这个问题千人千面,并没有一个官方解释。
函数式编程常用的核心概念:
声明式代码:
声明式的代码会描述一系列的操作,但是并不会暴露它们内部是如何实现的。
例如SQL语句就是一种很典型的声明式编程,它由一个个描述查询结果应该是什么样的断言组成,对数据检索的内部机制进行了抽象。
// 命令式
var makes = [];
for (var i = 0; i < cars.length; i++) {
makes.push(cars[i].make);
}
// 声明式
var makes = cars.map(function(car){ return car.make; });//map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
命令式的写法要求先实例化一个数组,然后再直接迭代cars列表,手动增加计数器,push进数组。它指明的是怎么做。
声明式的写法是一个表达式,如何进行计数器迭代、返回的数组如何收集,这些细节都隐藏了起来。它指明的是做什么,而不是怎么做。
不可变数据:
不可变数据是指创建后不能更改的数据。
javaScript里的基本类型(String、Number等)从本质上是不可变的,但是对象就是在任意的地方都可变。
var sortDesc = function(arr) {
return arr.sort(function(a, b) {
return b - a
})
}
var arr = [1, 3, 2]
sortDesc(arr) // [1, 2, 3]
arr // [1, 2, 3]
无副作用:
指的是调用函数不依赖于外部环境,不会产生附加的影响。这里的依赖可能是读取外部变量,也可能是修改外部变量、修改参数等。
一个函数如果依赖外部环境,那么它的行为就会变得不可预测。因为不知道外部环境什么时候就被改变了。
var counter = 0
function add() {
return ++counter // 有副作用。既引用了外部变量,也修改了外部变量
}
console.log(add())//1
console.log(add())//2
引用透明:
指一个函数只会用到传递给它的变量以及自己内部创建的变量,不会使用到其他变量。
var a = 1;
var b = 2;
// 函数内部使用的变量并不属于它的作用域
function test1() {
return a + b;
}
// 函数内部使用的变量是显式传递进去的
function test2(a, b) {
return a + b;
}
纯函数:
给定相同的输入,总是返回相同的输出,而且没有任何副作用的函数,叫做纯函数。
纯函数的优势在于,它保证了其"纯粹性",同样的输入无论调用多少次,都会是一样的输出,并且不用担心调用过程会修改外部环境。这让我们在做代码复用或者重构时,不用去担心函数是否会影响到其他地方。
slice()
:对于相同的输入,总是给出相同的输出,且不会修改到原数组,因此是纯函数。
splice()
:会修改到原数组,因此不是纯函数。
function fn(num1, num2) {
return num1 + num2 // 对于相同的输入,总是给出相同的输出,且执行过程中没有副作用
}
fn(10, 20)
函数柯里化:
传递给函数一部分参数來调用它,让它返回一个函数去处理剩下的函数,这个过程就称之为函数柯里化。
function fn1(x, y, z) {
console.log(x +y +z)
}
fn1(10, 20, 30)
// 函数柯里化之后:
function fn2(x) {
return function(y) {
return function(z) {
console.log(x +y + z)
}
}
}
fn2(10)(20)(30)
事实上,函数柯里化是一种预加载函数的方法,通过传递少量的参数,得到一个已经记住了这些参数的新函数,某种意义上来说,这是一种对参数的缓存,是一种比较高效的编写函数的方法;并且每个函数职责单一,还可以复用。但是,由于形成了闭包,也会造成内存泄漏。因此,不要滥用函数柯里化。
自动柯里化函数:
function fn(x, y, z) {
console.log(x +y +z)
}
// 自动函数柯里化:传入一个函数作为参数,返回一个新的柯里化之后的函数
function currying(fn) {
function curryFn(...args) {
// 如果传入柯里化之后的函数的参数个数已经大于等于原函数的参数个数,那么执行原函数
if (args.length >= fn.length) {
return fn(...args)
} else {
// 如果传入柯里化之后的函数的参数个数小于原函数的参数个数,那么返回一个新的函数,继续柯里化的过程
return function (...newArgs) {
return curryFn(...args.concat(newArgs))
}
}
}
return curryFn
}
var fnCurry = currying(fn)
fnCurry(10)(20)(30)
函数组合:
函数组合是将两个或更多的函数组合成一个新函数,依次调用的过程。是为了解决函数嵌套,形成洋葱代码 h(g(f(x)))
的问题。
例如:对一个数字先乘以 2,然后加 10。
如果操作很多,嵌套的函数就会非常深,而且代码是由内往外执行的,不直观。
function double(num) {
return num * 2
}
function pow(num) {
return num ** 2
}
console.log(pow(double(10))) // 形成了洋葱代码
可以将函数进行组合来解决。
// 将上面的两个函数组合在一个,返回一个新的函数
function composeFn(...fns) {
return function(...args) {
// 从左到右执行传入的函数,第一个函数接收传入的参数作为其参数,其他函数接收前一个函数的返回值作为参数
var result = fns[0](...args)
for (var i = 1; i < fns.length; i++) {
result = fns[i](result)
}
return result
}
}
const compose = composeFn(double, pow)
console.log(compose(10))
高阶函数:
高阶函数指的是一个函数以函数为参数,或以函数为返回值,或者既以函数为参数又以函数为返回值。
var add = function (a, b) {
return a + b;
};
function math(func, array) {
return func(array[0], array[1]);
}
math(add, [1, 2]); // 3
与面向对象编程的对比:
面向对象编程(OOP)的应用状态经常是共享的,并且和方法一起定义在一些对象中,所以会导致数据修改的不确定性。
函数式编程由于所有的数据都是不可变的,所以所有的变量在程序运行期间都是一直存在的,非常占用运行资源。同时由于函数式的先天性设计导致性能一直不够,运行速度也不够快。