JS 中常见的函数式编程
常见的编程范式
编程范式(Programming Paradigm)是指在编程中应用的一种思维、办法、格调以及标准。不同的编程范式对程序员的思维形式、程序的构造和实现形式都有影响。常见的编程范式有:
- 面向过程编程(Procedural Programming):以过程为核心,顺次实现各个步骤,在程序中应用变量、数组、构造体等数据结构。通常包含程序、分支和循环构造。C 语言就是一种面向过程的编程语言。
- 面向对象编程(Object-Oriented Programming):以对象为核心,将数据和办法封装在一个对象中,通过继承、多态等形式解决程序的复用和扩大问题。Java 和 C ++ 等语言就是一种面向对象的编程语言。
- 函数式编程(Functional Programming):强调应用纯函数和不可变数据,并尽可能减少对状态和可变数据的应用,从而防止副作用。Haskell 和 Clojure 等语言就是一种面向函数的编程语言。
- 逻辑式编程(Logic Programming):通过定义事实和规定,利用推理机制主动导出论断。Prolog 就是一种逻辑式编程语言。
- 申明式编程(Declarative Programming):通过申明程序的逻辑和目标,而不是明确指定每个步骤和如何实现工作来解决问题。HTML 和 CSS 就是一种申明式语言。
这些编程范式都有本人的长处和毛病,在实践中须要依据具体的利用场景和需要来抉择。同时,不同编程范式的组合和穿插也能够产生新的编程形式,如面向对象和函数式的联合产生了面向对象的函数式编程。
什么是函数式编程
函数式编程(Functional Programming)是一种编程范式,它强调应用纯函数(Pure Function)和不可变数据(Immutable Data),并尽可能减少对状态和可变数据的应用,从而防止副作用(Side Effect)。
在函数式编程中,函数被当作第一等公民,即函数能够作为参数传递给其余函数,也能够作为返回值返回给其余函数。函数式编程通常基于 λ 演算(Lambda Calculus)实践,其核心思想是无状态和数据不可变。
函数式编程次要利用于数学、迷信和工程计算等畛域。
函数式编程范式的特点如下:
- 函数是一等公民:函数能够作为参数传递给其余函数,也能够作为返回值返回给其余函数。这能够不便地应用高阶函数,实现代码复用和简化代码。函数式编程的目标是让程序更加简洁,可读性更高,易于测试和保护,最终进步代码的品质和效率。在函数式编程中,函数被视为独立的工具,能够像数学中的函数一样进行组合,以实现简单的计算工作。
- 无状态和数据不可变:在函数式编程中,咱们不能扭转已有的数据,只能通过函数的计算生成新的数据。这是为了防止数据状态的不确定性,从而更容易实现并发执行。这种形式使代码不依赖于内部状态,缩小了出错的机会,进步了代码的可读性和可维护性。
- 可组合性和可复用性:函数式编程中的函数通常只依赖于它的输出,与外部环境无关,因而函数之间能够互相组合和复用,从而实现更加拆分的代码。
- 惰性计算和有限序列:函数式编程通常应用惰性计算和有限序列等技术,从而实现更为高效和灵便的算法。
函数式编程罕用的技术和办法包含高阶函数、纯函数、柯里化、函数组合、惰性求值等。而在 JavaScript 中,也提供了函数式编程的反对,例如 ES6 中的箭头函数、高阶函数、Map/Reduce 等办法,以及函数式库 Lodash 和 Underscore.js,还有 React 和 Redux 等也采纳了函数式编程的思维。罕用的函数式编程语言有 Lisp、Haskell、Erlang、Clojure 等。
函数式编程是一种十分弱小的编程模式,它提供了一种对数据进行纯函数操作的范式,让程序员可能更加容易地了解和维护程序。它实用于那些须要解决大量数据的场景,同时也对于简化可并发零碎的设计有着很大的帮忙。
函数式编程罕用的应用场景
高阶函数
高阶函数指的是能够接管函数作为参数或者返回一个新函数的函数。在 JS 中,罕用的高阶函数有 map、reduce、filter 等,这些函数能够不便地解决数组和对象等数据类型。
上面是一个应用 JS 实现高阶函数的示例代码:
// 定义一个高阶函数,接管一个函数作为参数,并调用它
function higherOrderFunc(func) {console.log("调用高阶函数");
func();}
// 定义一个函数作为参数
function someFunc() {console.log("调用了函数作为参数");
}
// 调用高阶函数,并传入函数作为参数
higherOrderFunc(someFunc);
在这个例子中,咱们定义了一个高阶函数 higherOrderFunc
,它接管一个函数作为参数,并在外部调用它。咱们还定义了一个函数 someFunc
,并将其作为参数传递给 higherOrderFunc
。当 higherOrderFunc
被调用时,它首先打印一条音讯,而后调用传入的函数。
除了接管一个函数作为参数外,高阶函数还能够返回一个函数。上面是一个示例,它创立并返回一个新函数:
// 定义一个返回函数的高阶函数
function createNewFunction() {console.log("创立新的函数");
// 返回一个新函数
return function() {console.log("这是一个新的函数");
};
}
// 调用 createNewFunction()函数,它返回一个函数
const newFunc = createNewFunction();
// 调用返回的函数
newFunc();
在这个示例中,咱们定义了一个高阶函数 createNewFunction
。它打印一条音讯,而后返回一个新函数。咱们还将返回的函数存储在 newFunc
中,并在接下来的代码中调用它。
这样就实现了利用 JS 实现函数式编程中的高阶函数。
纯函数
纯函数指的是对于雷同的输出,总是返回雷同的输入,而且没有任何副作用的函数。在 JS 中,咱们通常会应用纯函数来确保程序的可靠性和可维护性。
上面是一个非纯函数和纯函数的示例代码比照:
// 非纯函数
function nonPureAdd(arr, num) {arr.push(num); // 批改了函数内部的数据
return arr;
}
const numbers = [1, 2, 3];
nonPureAdd(numbers, 4);
console.log(numbers); // [1, 2, 3, 4]
// 纯函数
function pureAdd(arr, num) {const newArray = [...arr]; // 创立了新的数组正本,不扭转原有数据
newArray.push(num);
return newArray;
}
const numbers2 = [1, 2, 3];
pureAdd(numbers2, 4);
console.log(numbers2); // [1, 2, 3]
在这个例子中,非纯函数 nonPureAdd
承受一个数组和一个数字,而后将数字增加到该数组中并返回批改后的数组。留神,在这个过程中,咱们批改了原始数组,这意味着函数是非纯的。另一方面,纯函数 pureAdd
承受同样的参数,然而它不会扭转原始数组,并且会返回一个新的数组。因为它不依赖于任何内部状态并且不会扭转任何内部状态,所以它是纯函数。
正文阐明:
- 非纯函数:
nonPureAdd
函数间接扭转了传入的数组并返回了扭转后的数组 - 纯函数:
pureAdd
函数并没有扭转传入的数组,而是创立了一个新数组存储了原数组数据并将新数字增加进去,最初返回的新数组
应用场景:
- 纯函数的次要劣势是可靠性和可重复性,因为它们不依赖于上下文或内部状态,所以在并行计算或单元测试等场景下十分有用。
- 非纯函数通常用于须要依赖内部状态的场景,例如操作 DOM 或进行网络申请等。不过,要留神非纯函数的后果可能会在同样的输出下具备不同的后果,它们也更难以测试和调试。
须要阐明的是,这里只是以示例代码的形式简略阐明纯函数和非纯函数的差异,理论中还须要依据具体的场景进行应用和设计。
柯里化
函数柯里化(Currying)是一种函数式编程的技术,通过把接管多个参数的函数转化为接管一个参数并返回新函数的模式,来简化函数的调用和应用。实现函数柯里化须要用到闭包和递归。
上面是一个残缺示例代码:
// 定义一个柯里化函数
function curry(fn) {return function curried(...args) {if (args.length >= fn.length) {
// 如果传入参数个数达到函数的形参个数,则间接调用该函数
return fn.apply(this, args);
} else {
// 如果传入参数有余,则返回一个接管残余参数的新函数
return function (...rest) {return curried.apply(this, args.concat(rest));
};
}
};
}
// 定义一个一般函数,用于演示柯里化
function add(x, y, z) {return x + y + z;}
// 应用柯里化给 add 函数增加复用性
const curriedAdd = curry(add);
// 调用柯里化函数
console.log(curriedAdd(1, 2, 3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6
这个函数接管一个函数作为参数,并返回一个新的函数。新函数会接管第一个参数,并返回一个新的函数,只有参数数量不够,就始终返回新函数。直到参数数量满足原函数的要求时,才会真正调用原函数。
上面是对这个函数的应用场景进行一些解释:
- 针对某些函数须要屡次调用,而每次调用都有一些雷同的参数,能够应用柯里化来不便地实现:
function add(a, b, c) {return a + b + c;}
let curriedAdd = curry(add);
let addTwoNumbers = curriedAdd(2);
let result = addTwoNumbers(3, 4); // 9
在下面的代码中,curriedAdd
是 add
函数的柯里化版本。咱们首先调用 curriedAdd(2)
返回一个新的函数 addTwoNumbers
,这个函数只须要接管两个参数,而第一个参数指定了 a
的值为 2。所以在前面的调用中,咱们只须要传递两个参数即可失去后果。
- 将函数桥接起来,能够更好地复用代码。
function func1(a, b) {return (a + 1) * b;
}
function func2(a, b) {return a + b;}
let curriedFunc1 = curry(func1);
let curriedFunc2 = curry(func2);
let result = curriedFunc2(1, curriedFunc1(2, 3)); // 10
在上述代码中,咱们用柯里化把 func1
转化成须要一个参数的函数,并用它桥接了 func2
的第一个参数。这样,咱们就能够复用 func1
的返回值,并且不须要提前执行它。
柯里化能够带来许多益处,包含代码模块化、代码重用、代码简洁和可读性等。
函数组合
函数组合是将多个函数联合在一起,造成一个新的函数,这个新函数会顺次调用组合的函数。在 JS 中,咱们能够应用函数的 compose 和 pipe 办法来实现函数的组合。
示例代码如下:
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
// 假如有两个函数
const add = x => x + 1
const multiply = x => x * 2
// 组合这两个函数
const addAndMultiply = compose(multiply, add)
// 应用组合函数
const result = addAndMultiply(2) // the result will be 6
上述代码中,compose
函数接管多个函数作为参数,并返回一个新函数。将这个新函数利用于某个值,能够将其利用于所有传入函数,依照最左边的函数到最右边的函数的程序。
这里 addAndMultiply
变量寄存的是将 add
函数和 multiply
函数以组合形式传入 compose
函数中失去的函数,该函数接管一个参数,在该示例中是 2
。解释一下该函数的作用是:将参数 2
传递给 add
函数,将后果传递给 multiply
函数,并返回它的后果。
函数组合在函数式编程中十分常见。应用函数组合的场景之一是简化在一次函数调用中执行多个操作,并确保这些操作依照特定的秩序执行。能够将简单的操作合成为不同的函数,而后将它们连接起来以在一次调用中执行它们。这样的代码比间接实现这些操作的代码更易于浏览和了解。
惰性求值
惰性求值是指在须要后果的时候才计算,在不必要时防止计算。这能够进步性能,特地是在波及到大量计算的状况下。惰性求值在 JavaScript 中通过函数来实现。
一般来说,应用惰性求值会波及到某些初始化操作。如果这些操作能够推延到产生更重要的事件产生之前,就能够进步性能。上面是一个残缺且优化后的示例代码:
let add = function (a, b) {console.log("Adding two numbers")
return a + b
}
let lazyAdd = function (a, b) {console.log("Adding two numbers (lazily)")
let sum = null // 缓存计算结果
return function () {if (sum === null) {sum = a + b}
return sum
}
}
let a = add(1, 2) // "Adding two numbers"
let b = lazyAdd(1, 2)() // "Adding two numbers (lazily)"
上述代码中,add
函数立刻计算并返回两个数的和,而 lazyAdd
函数返回一个函数,该函数计算和。在这个实现中,应用了一个变量 sum
来缓存计算结果。只有在 sum
为空时才进行理论计算,以防止反复计算。
应用惰性求值有多种场景,其中最常见的是在理论计算成本较高的中央。例如,如果要进行简单的计算或与数据库进行通信,则推延这些计算或通信操作可能会进步性能,因为它们只有在须要后果时才会产生。另一个常见的利用是在解决元素事件时,提早事件处理能够进步程序的响应速度和性能。
尾递归
尾递归是指一个函数的最初一步操作是调用本身,也就是说,在函数的最初一次调用中,它不再执行任何操作,间接返回该调用的后果作为整个函数的后果。这样的递归称为尾递归。
尾递归的特点是,它能够防止在递归过程中生成大量的调用堆栈,从而减小程序的内存耗费,进步代码的性能和运行效率。
举个例子,比方上面这个阶乘函数:
function factorial(n) {if (n === 1) {return 1;} else {return n * factorial(n - 1);
}
}
console.log(factorial(5)) // 120
这个函数的最初一步操作是将 n
乘以 factorial(n - 1)
的后果,而后返回这个乘积。因而,这个递归是不是尾递归呢?答案是不是的,因为即便递归的后果被乘以了n
,但在返回后果之前,还要将这个乘积压入调用堆栈中,最初实现所有调用后再返回。因而,在递归深度十分大的状况下,应用这种非尾递归的形式,会导致调用堆栈溢出或耗费大量内存。
为了防止这个问题,咱们能够应用尾递归的形式来实现计算阶乘函数:
function factorial(n, accumulator = 1) {if (n === 1) {return accumulator;}
return factorial(n - 1, n * accumulator);
}
console.log(factorial(5)) // 120
解释一下这段代码:该函数承受两个参数,其中 n 示意要计算阶乘的自然数,accumulator 示意通过递归传递下来的两头后果。函数首先判断 n 是否为 1,如果是则返回 accumulator,否则递归调用本身,并将 n - 1 和 n *accumulator 作为参数传递上来。在递归回来的过程中,accumulator 会一直乘高低传的 n 值,直到计算完 n 的阶乘。
这个函数的最初一步操作是:调用本身,并将新的参数传递给它。因而,这个递归是尾递归。在这个尾递归算法中,咱们将累加器 accumulator
作为一个额定参数传递给下一次函数调用,而不是递归调用乘法运算,这样能够防止在递归过程中生成大量的调用堆栈。这个算法尽管看起来更简单一些,但却可能更好地解决大规模数据集,并防止调用堆栈溢出和内存耗费的问题。
尾调用和尾递归的区别
尾调用和尾递归都是基于函数调用栈的优化形式,两者的区别在于尾调用是指调用函数时,在该语句所在函数的返回值处调用另一个函数,并且在此之后不再有任何操作。
而尾递归则是一种非凡的尾调用,在递归调用时,调用本身并且返回值是函数调用的返回值,此时总是在函数的最初一步进行递归调用,因而也叫做“尾递归”。
尾调用和尾递归的区别在于,尾调用能够调用任何函数,而尾递归则必须是对本身的调用。另外,尾递归能够通过一些编译器的优化,将递归调用转化为迭代循环,从而防止了递归过程中的函数调用栈溢出问题,进步了效率。
因而,尾递归是一种更加高效的递归形式,对于须要进行长时间递归计算的问题,采纳尾递归可能更好地解决递归栈溢出等问题。
以上是 JS 中罕用的函数式编程,通过这些办法能够让咱们更加优雅地解决数据和逻辑。
函数式编程的优缺点
函数式编程具备以下长处:
- 更清晰的代码构造:函数式编程的代码通常能够拆分成许多小的、性能繁多的纯函数,更加可读、易了解、便于保护。
- 更大的可扩展性和可重用性:函数式编程的代码往往只依赖于其输出,因而能够很容易地组合和重用函数。
- 更高的代码可靠性:函数式编程强调数据不可变性和无状态,防止了常见的问题,如意外批改数据、并发问题等。
- 更好的并行性:因为函数式编程的纯函数无状态和数据不可变性,因而能够不便地进行并行处理。
- 副作用少:函数式编程办法通常不会产生副作用,如扭转变量、批改对象状态等,因而可缩小代码的复杂性。
然而,函数式编程的毛病也很显著:
- 形象水平高:函数式编程往往须要应用形象的办法和符号,有时较难了解。
- 在某些状况下效率较低:函数式编程办法通常不会扭转数据,而是生成新的数据,这在解决大量数据时会产生更多的开销。
- 不如命令式编程直观:命令式编程更靠近人类天然的思维形式,而函数式编程则须要更多的思考和学习,对于初学者较为艰难。
总的来说,函数式编程具备很多长处,如果能很好地把握这种编程办法,就可写出更加清晰、可读性更强、可维护性更高的代码。但对于一些简单的利用,与面向对象编程相比,可能须要一些非凡的技巧来应答。
本文由 mdnice 多平台公布