函数式编程概述 函数式编程强调使用函数进行计算,避免使用可变状态和副作用 。它依赖于数学函数的概念,即相同的输入始终会产生相同的输出 。
编程范式
函数式编程(Functional Programming, FP),FP是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。
JavaScript支持多种编程范式,包括命令式、面向对象和函数式编程。
常见编程范式 面向过程编程(Procedural Programming)
面向对象编程(Object-Oriented Programming, OOP)
函数式编程(Functional Programming)
其他编程范式
元编程(Metaprogramming)
声明式编程(Declarative Programming)
通过描述 “做什么” 而不是 “怎么做”,常用于数据库查询和 HTML。典型代表是 SQL、XQuery、React。
特点:逻辑性、表达数据关系而非操作步骤。
并发编程(Concurrent Programming)
逻辑编程(Logic Programming)
面向事件编程(Event-Driven Programming)
反应式编程(Reactive Programming)
领域特定语言(Domain-Specific Languages, DSL)
函数式编程优点
可预测性 :因为函数是纯粹的,给定相同的输入,它们总是会产生相同的输出。
更容易调试和测试 :由于函数没有副作用,因此函数的行为是可预测的,更容易单元测试。
代码简洁和可组合性强 :函数可以通过组合来创建复杂的操作。
举个栗子:
1 2 3 4 5 const num1 = 2 const num2 = 3 const sum = num1 + num2console .log (sum)
1 2 3 4 5 6 function add (n1, n2 ) { return n1 + n2 } const sum = add (2 , 3 )console .log (sum)
必知基本概念 纯函数
纯函数(Pure Function) 是指满足以下两个条件的函数:
确定性 :纯函数对于相同的输入,总是返回相同的输出。
无副作用 :纯函数不会改变外部状态,也不会依赖外部的可变状态。
纯函数的特点
无副作用 :函数不依赖也不修改函数外部的状态,不会影响或依赖外部的任何变量、数据结构、数据库等。副作用通常包括修改全局变量、改变传入的对象、I/O 操作等。
可缓存性 (Memoization) :由于纯函数总是返回相同的输出,因此可以对纯函数的输出进行缓存,从而在将来相同的输入下直接返回缓存的结果,提高效率。
易测试性 :纯函数只依赖输入参数进行计算,测试时无需设置复杂的环境或模拟外部依赖。
并发和并行编程的安全性 :纯函数不依赖于共享状态,也不会修改外部状态,因此在并发或并行编程环境中可以安全地使用而不必担心数据竞争或竞态条件。
可组合性 :纯函数可以轻松组合在一起构建更复杂的函数。例如,函数组合(Function Composition)就是将多个纯函数组合成一个新函数。
纯函数的例子 1 2 3 4 5 function doubleArray (arr ) { return arr.map (x => x * 2 ); }
可缓存性的例子:记忆化
使用Lodash中的memoize
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const _ = require ('lodash' );function expensiveCalculation (num ) { console .log (`Calculating ${num} ...` ); return num * 2 ; } const memoizedCalculation = _.memoize (expensiveCalculation);console .log (memoizedCalculation (5 )); console .log (memoizedCalculation (5 )); console .log (memoizedCalculation (10 ));
自己动手搓一个
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 function memoize (fn ) { const cache = new Map (); return function (...args ) { const key = JSON .stringify (args); if (cache.has (key)) { return cache.get (key); } const result = fn (...args); cache.set (key, result); return result; }; } function expensiveCalculation (num ) { console .log (`Calculating ${num} ...` ); return num * 2 ; } const memoizedCalculation = memoize (expensiveCalculation);console .log (memoizedCalculation (5 )); console .log (memoizedCalculation (5 )); console .log (memoizedCalculation (10 ));
将非纯函数改造成纯函数
移除外部状态依赖 :将外部状态作为参数传递给函数,而不是直接在函数内部引用外部变量。
避免修改传入的参数 :如果需要改变数据结构,可以返回一个新的数据结构,而不是在原有数据结构上进行修改。
将I/O操作与计算逻辑分离 :将副作用代码(如I/O操作)与核心计算逻辑分开处理。例如,可以将I/O操作放在函数外部,传入数据给纯函数进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 function updateObject (obj, key, value ) { obj[key] = value; return obj; } function updateObjectPure (obj, key, value ) { return { ...obj, [key]: value }; }
不可变性
不可变性(Immutability) 是指一旦创建的数据对象,其状态不可被改变 。不可变的数据结构在修改时,实际上会返回一个新的数据结构,原数据结构保持不变 。
不可变性的优点
减少副作用 :不可变性保证了数据结构不会被修改 ,从而避免了因为数据的变化而产生的副作用。这使得函数变得更加可靠和可预测。
提高代码可读性和可维护性 :不可变的数据结构简化了数据的跟踪和管理,使代码更容易理解和维护。因为数据结构的变化不影响原有的数据,所以逻辑更简单。
易于调试和测试 :由于数据在函数调用期间不会改变,容易追踪数据的变化,从而简化了调试过程。纯函数和不可变数据结构结合起来,使得测试变得更容易。
支持并发编程 :不可变数据结构在多线程或异步环境中特别有用,因为多个线程或异步操作可以安全地访问相同的数据而不需要锁定或同步机制。
数据结构版本控制 :不可变性使得我们可以轻松实现数据结构的版本控制。通过维护不同版本的不可变数据结构,可以进行历史追溯和回滚操作。
不可变数据结构的实现 原生 JavaScript 对象和数组 在原生 JavaScript 中,对象和数组是可变的。对于需要不可变特性的应用,常用方法包括:
对象赋值:使用对象展开运算符 (...
) 创建新对象。
数组操作:使用数组方法 (concat
, slice
, map
, filter
) 返回新数组。
1 2 3 4 5 6 const person = { name : 'Alice' , age : 25 };const updatedPerson = { ...person, age : 26 };console .log (updatedPerson); console .log (person);
1 2 3 4 5 6 const numbers = [1 , 2 , 3 ];const newNumbers = [...numbers, 4 ];console .log (newNumbers); console .log (numbers);
使用不可变数据结构库 Immutable.js Immutable.js 提供了不可变的数据结构,如 List
、Map
、Set
等,这些数据结构在修改时会返回新的实例,原始数据结构保持不变。
1 2 3 4 5 6 7 const { Map } = require ('immutable' );const person = Map ({ name : 'Alice' , age : 25 });const updatedPerson = person.set ('age' , 26 );console .log (updatedPerson.toJS ()); console .log (person.toJS ());
Immer Immer 允许你以可变的方式编写代码,但会自动生成不可变的更新。它通过使用“草稿”来处理数据,简化了不可变数据结构的操作。
1 2 3 4 5 6 7 8 9 10 const produce = require ('immer' );const person = { name : 'Alice' , age : 25 };const updatedPerson = produce (person, draft => { draft.age = 26 ; }); console .log (updatedPerson); console .log (person);
引用透明性
引用透明性(Referential Transparency) 是指一个表达式在程序的任何地方都可以被其值所替代,而不改变程序的行为 。这意味着如果你将一个表达式替换成它的值,程序的结果和行为应当保持不变。
引用透明性的优点
可替代性: 如果一个表达式在程序中可以被它的计算结果所替代而不改变程序的结果,这个表达式就是引用透明的。换句话说,表达式的结果在所有上下文中都是一致的 。
简化代码理解: 由于引用透明性保证了表达式的一致性,代码的理解和维护变得更加容易。你可以在代码的任意位置替换表达式而不会影响程序的行为 。
优化和重构的支持: 引用透明性使得代码的优化和重构变得更加安全和可靠。例如,编译器可以进行代数重写和优化,因为它可以安全地用表达式的值替代表达式。
引用透明性的例子
1 2 3 4 5 6 7 8 9 function add (x, y ) { return x + y; } const result1 = add (2 , 3 );const result2 = 2 + 3 ; console .log (result1 === result2);
1 2 3 const pi = 3.14159 ;const radius = 5 ;const area = pi * radius * radius;
1 2 3 4 5 6 7 8 9 10 11 12 let x = 10 ;function multiplyByTwo ( ) { x *= 2 ; return x; } const result1 = multiplyByTwo (); const result2 = multiplyByTwo ();
函数作为一等公民
函数作为一等公民(First-Class Citizen) 或 一等对象(First-Class Object) 指的是函数在语言中被当作一种基本的数据类型对待 。
可以作为变量赋值 函数可以被赋值给变量,从而可以通过变量调用该函数。
1 2 3 4 5 6 const greet = function (name ) { return `Hello, ${name} !` ; }; console .log (greet ('Alice' ));
可以作为参数传递 函数可以作为参数传递给其他函数,允许更高层次的抽象和操作。
1 2 3 4 5 6 7 8 9 10 11 function processUserInput (callback ) { const name = prompt ('Please enter your name.' ); callback (name); } function greet (name ) { console .log (`Hello, ${name} !` ); } processUserInput (greet);
可以作为返回值 函数可以作为另一个函数的返回值,这允许函数生成其他函数。
1 2 3 4 5 6 7 8 9 function multiplier (factor ) { return function (x ) { return x * factor; }; } const double = multiplier (2 );console .log (double (5 ));
可以被存储在数据结构中 函数可以被存储在数组、对象等数据结构中,并可以动态调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const operations = [ function (x ) { return x + 1 ; }, function (x ) { return x * 2 ; }, function (x ) { return x - 3 ; } ]; function applyOperations (value, ops ) { return ops.reduce ((acc, operation ) => operation (acc), value); } const initialValue = 5 ;const result = applyOperations (initialValue, operations);console .log (result);
可以被动态创建和操作 可以在运行时创建函数,并动态生成或修改函数。
1 2 3 4 5 6 7 8 9 10 const createFunction = (operator ) => { if (operator === 'add' ) { return (a, b ) => a + b; } else if (operator === 'subtract' ) { return (a, b ) => a - b; } }; const addFunction = createFunction ('add' );console .log (addFunction (3 , 4 ));
高阶函数
高阶函数(Higher-Order Function) 是指接收一个或多个函数作为参数,或返回一个函数的函数 。
函数作为参数 map
函数是一个常见的高阶函数,它接收一个函数和一个数组作为参数,对数组中的每个元素应用这个函数,并返回一个新数组。
1 2 3 4 5 6 const numbers = [1 , 2 , 3 , 4 , 5 ];const squares = numbers.map (x => x * x);console .log (squares);
函数作为返回值 1 2 3 4 5 6 7 8 9 10 11 12 function createMultiplier (factor ) { return function (x ) { return x * factor; }; } const double = createMultiplier (2 );console .log (double (5 ));
函数组合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function compose (f, g ) { return function (x ) { return f (g (x)); }; } function double (x ) { return x * 2 ; } function square (x ) { return x * x; } const doubleThenSquare = compose (square, double);console .log (doubleThenSquare (3 ));
实现一些常见的高阶函数 forEach
1 2 3 4 5 6 7 8 9 10 11 const forEach = (array, fn ) => { for (let arr of array) { fn (arr) } } const arr = [1 , 3 , 4 , 7 , 8 ]forEach (arr, function (item ) { console .log (item) })
map
1 2 3 4 5 6 7 8 9 10 11 const map = (array, fn ) => { let results = [] for (let arr of array) { results.push (fn (arr)) } return results } const arr = [1 , 3 , 4 , 7 , 8 ]console .log (map (arr, (item ) => item * item))
filter
1 2 3 4 5 6 7 8 9 10 11 const filter = (array, fn ) => { let results = [] for (let arr of array) { fn (arr) && results.push (arr) } return results } const arr = [1 , 3 , 4 , 7 , 8 ]console .log (filter (arr, (item ) => item > 3 ))
every
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const every = (array, fn ) => { let result = true for (let arr of array) { result = fn (arr) if (!result) { break } } return result } const arr = [1 , 3 , 4 , 7 , 8 ]console .log (every (arr, (item ) => item > 3 ))
some
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const some = (array, fn ) => { let result = true for (let arr of array) { result = fn (arr) if (result) { break } } return result } const arr = [1 , 3 , 4 , 7 , 8 ]console .log (some (arr, (item ) => item > 3 ))
函数式编程中的重要概念与技术 函数组合
函数组合(Function Composition) 是指将两个或多个函数组合成一个新的函数,使得新的函数依次应用这些函数。简单来说,函数组合就是“把函数拼接在一起”。
1 2 3 4 5 const compose = (f, g ) => x => f (g (x));const double = x => x * 2 ;const increment = x => x + 1 ;const doubleThenIncrement = compose (increment, double);console .log (doubleThenIncrement (5 ));
手动实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function compose (...functions ) { return function (x ) { return functions.reduceRight ((acc, fn ) => fn (acc), x); }; } const double = x => x * 2 ;const square = x => x * x;const doubleThenSquare = compose (square, double);console .log (doubleThenSquare (3 ));
插播一条:reduce
的实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Array .prototype .reduce = function (array, fn, initialValue ) { let accumlator; if (initialValue != undefined ) { accumlator = initialValue } else { accumlator = array[0 ] } if (initialValue === undefined ) { for (let i = 1 ; i < array.length ; i++) { accumlator = fn (accumlator, array[i]) } } else { for (let arr of array) { accumlator = fn (accumlator, arr) } } return [accumlator] }
使用 Ramda 实现 1 2 3 4 5 6 7 8 9 10 const R = require ('ramda' );const double = x => x * 2 ;const square = x => x * x;const doubleThenSquare = R.compose (square, double);console .log (doubleThenSquare (3 ));
管道
管道操作符 |>
(提案中的特性) 用于将一个表达式的输出传递给下一个表达式作为输入。
管道是一种数据处理模式,其中数据从一个处理阶段流入下一个阶段,每个阶段都对数据进行某种处理。最终,数据经过所有阶段的处理,得到最终结果。
手动实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function pipe (...functions ) { return function (x ) { return functions.reduce ((acc, fn ) => fn (acc), x); }; } const double = x => x * 2 ;const square = x => x * x;const increment = x => x + 1 ;const processNumber = pipe (double, square, increment);console .log (processNumber (3 ));
Lodash的_.flow
和 _.flowRight
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const _ = require ('lodash' );const double = x => x * 2 ;const square = x => x * x;const increment = x => x + 1 ;const processNumber = _.flow ([ double, square, increment ]); console .log (processNumber (3 ));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const _ = require ('lodash' );const double = x => x * 2 ;const square = x => x * x;const increment = x => x + 1 ;const processNumber = _.flowRight ([ increment, square, double ]); console .log (processNumber (3 ));
柯里化
柯里化是将一个接受多个参数的函数转化为接受一个参数的函数,并返回接受其余参数的新函数的过程 。
简易的柯里化:
1 2 3 4 5 6 7 8 const curry = (fn ) => (...args ) => args.length >= fn.length ? fn (...args) : curry (fn.bind (null , ...args)); const add = (a, b ) => a + b;const curriedAdd = curry (add);console .log (curriedAdd (1 )(2 ));
详细请见:JavaScript 中的函数柯里化 (Currying) | 灰太羊的羊村 (huitaiyang.top)
偏应用
偏应用(Partial Application) 是指创建一个新函数,该函数从原函数中继承部分参数。通过偏应用,可以将原函数的某些参数固定下来,生成一个新的函数,这个新函数只需提供剩余的参数即可调用 。
与柯里化的区别 :柯里化是逐个应用参数,而偏应用是一次应用部分参数。
偏应用的实现 手动实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function partial (fn, ...partialArgs ) { return function (...args ) { return fn (...partialArgs, ...args); }; } function multiply (a, b, c ) { return a * b * c; } const multiplyBy2And3 = partial (multiply, 2 , 3 );console .log (multiplyBy2And3 (4 ));
使用 bind
实现偏应用 1 2 3 4 5 6 7 8 9 function greet (greeting, name ) { return `${greeting} , ${name} !` ; } const greetHello = greet.bind (null , 'Hello' );console .log (greetHello ('World' ));
偏应用的应用 简化函数调用 通过偏应用,可以简化函数调用,避免重复传递相同的参数。
1 2 3 4 5 6 7 8 9 10 11 function sendEmail (to, subject, body ) { console .log (`Sending email to ${to} with subject ${subject} ` ); } const sendWelcomeEmail = partial (sendEmail, 'Welcome!' );sendWelcomeEmail ('user@example.com' );
函数组合 偏应用可以与函数组合结合使用,提高代码的灵活性和可读性。
1 2 3 4 5 6 7 8 9 10 11 const add = x => y => x + y;const multiply = x => y => x * y;const add10 = add (10 );const multiplyBy3 = multiply (3 );const calculate = pipe (add10, multiplyBy3);console .log (calculate (5 ));
闭包
闭包(Closure) 是指在 JavaScript 中,一个函数能够记住并访问它定义时的作用域(即其词法环境)的机制,即使这个函数在其定义的作用域之外被调用。简单来说,闭包允许函数访问其创建时的外部作用域中的变量 。
在 JavaScript 中,函数在定义时会形成一个作用域链。闭包利用这个作用域链来访问函数外部的变量。
闭包是一个函数加上其环境。环境包含函数定义时可用的所有变量。
闭包可以保存其创建时的状态,这使得函数能够持久地访问和操作外部变量。
闭包的本质 : 函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放 ,因此内部函数依然可以访问外部函数的成员
符合以下两点即是闭包:
在该函数外部,对该函数内部有引用
在另一个作用域访问到 该函数作用域中的局部成员
闭包有3个可访问的作用域:
在它自身声明之内声明的变量
对全局变量的访问
对外部函数变量的访问
闭包的例子 只执行一次(Once) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function once (fn ) { let done = false return function ( ) { if (!done) { done = true return fn.apply (fn, arguments ) } else { console .log (`函数${fn.name} 只能调用一次!` ) } } } function pay (money ) { console .log (`支付${money} 元` ) } const payOnce = once (pay)payOnce (5 )payOnce (5 )
私有数据管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function createPerson (name ) { let age = 0 ; return { getName : function ( ) { return name; }, getAge : function ( ) { return age; }, setAge : function (newAge ) { if (newAge > age) { age = newAge; } } }; } const person = createPerson ('John' );console .log (person.getName ()); console .log (person.getAge ()); person.setAge (25 ); console .log (person.getAge ());
函数工厂 1 2 3 4 5 6 7 8 9 10 11 function multiplyBy (factor ) { return function (x ) { return x * factor; }; } const double = multiplyBy (2 );const triple = multiplyBy (3 );console .log (double (5 )); console .log (triple (5 ));
模块模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const CounterModule = (function ( ) { let count = 0 ; return { increment ( ) { count++; return count; }, decrement ( ) { count--; return count; }, getCount ( ) { return count; } }; })(); console .log (CounterModule .increment ()); console .log (CounterModule .increment ()); console .log (CounterModule .decrement ()); console .log (CounterModule .getCount ());
延迟执行 1 2 3 4 5 6 7 8 9 10 function delayMessage (message, delay ) { return function ( ) { setTimeout (() => { console .log (message); }, delay); }; } const delayedHello = delayMessage ('Hello after 2 seconds' , 2000 );delayedHello ();
记忆化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function memoize (fn ) { const cache = {}; return function (...args ) { const key = JSON .stringify (args); if (cache[key]) { return cache[key]; } const result = fn (...args); cache[key] = result; return result; }; } const factorial = memoize (function (n ) { if (n <= 1 ) return 1 ; return n * factorial (n - 1 ); }); console .log (factorial (5 )); console .log (factorial (6 ));
自定义计时器 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 function createTimer ( ) { let startTime = 0 ; let elapsedTime = 0 ; let timerId; function start ( ) { startTime = Date .now (); timerId = setInterval (() => { elapsedTime = Date .now () - startTime; console .log (`Elapsed time: ${elapsedTime} ms` ); }, 1000 ); } function stop ( ) { clearInterval (timerId); } function reset ( ) { elapsedTime = 0 ; startTime = Date .now (); } return { start, stop, reset }; } const timer = createTimer ();timer.start (); setTimeout (timer.stop , 5000 );
生成唯一ID 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function createIdGenerator ( ) { let id = 0 ; return function ( ) { id += 1 ; return id; }; } const generateId = createIdGenerator ();console .log (generateId ()); console .log (generateId ()); console .log (generateId ());
事件订阅/发布模式 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 function createPubSub ( ) { const subscribers = {}; return { subscribe (event, callback ) { if (!subscribers[event]) { subscribers[event] = []; } subscribers[event].push (callback); }, publish (event, data ) { if (subscribers[event]) { subscribers[event].forEach (callback => callback (data)); } } }; } const pubsub = createPubSub ();pubsub.subscribe ('message' , (data ) => { console .log (`Received message: ${data} ` ); }); pubsub.publish ('message' , 'Hello, World!' );
状态机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function createStateMachine (initialState ) { let state = initialState; function transition (newState ) { state = newState; console .log (`State changed to: ${state} ` ); } function getState ( ) { return state; } return { transition, getState }; } const fsm = createStateMachine ('idle' );console .log (fsm.getState ()); fsm.transition ('running' ); console .log (fsm.getState ()); fsm.transition ('stopped' );
动态模板渲染 1 2 3 4 5 6 7 8 9 10 11 function createTemplateRenderer (template ) { return function (data ) { return template.replace (/{{(\w+)}}/g , (match, key ) => data[key] || '' ); }; } const template = 'Hello, {{name}}! Welcome to {{location}}.' ;const render = createTemplateRenderer (template);console .log (render ({ name : 'John' , location : 'New York' })); console .log (render ({ name : 'Jane' , location : 'San Francisco' }));
自定义事件系统 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 function createEventEmitter ( ) { const events = {}; function on (event, listener ) { if (!events[event]) { events[event] = []; } events[event].push (listener); } function off (event, listener ) { if (events[event]) { events[event] = events[event].filter (l => l !== listener); } } function emit (event, data ) { if (events[event]) { events[event].forEach (listener => listener (data)); } } return { on, off, emit }; } const emitter = createEventEmitter ();function responseHandler (data ) { console .log (`Response received: ${data} ` ); } emitter.on ('response' , responseHandler); emitter.emit ('response' , 'Success' ); emitter.off ('response' , responseHandler); emitter.emit ('response' , 'Success' );
递归
递归 是编程中一种常见的技术,它指的是函数调用自身 来解决问题。递归通常用于分解复杂的问题,将其拆解为更小、更易解决的子问题,然后将这些子问题的结果组合起来得到最终结果。
递归分为直接递归 和间接递归 :
直接递归 :函数直接调用自身。
间接递归 :函数通过其他函数间接调用自身。
递归函数通常包含两个主要部分:
基线条件(Base Case) :防止无限递归,是递归停止的条件。
递归条件(Recursive Case) :函数继续调用自身,缩小问题的规模。
尾调用
尾调用 (Tail Call)是指在一个函数的最后一步调用另一个函数(包括调用自身),并且函数的 返回结果直接是这个调用的返回值 。在尾调用的情况下,当前函数的执行完成之后不需要做其他操作,返回值就是尾调用函数的返回值。
1 2 3 4 5 6 7 8 9 function f (x ) { return g (x); } function g (x ) { return x + 1 ; } console .log (f (5 ));
注意:尾调用最后只能是返回函数调用,而不能是别的。 以下f1
,f2
,f3
都不是尾调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function g (x ) { return x } function f1 (x ) { return g (x) + 1 } function f2 (x ) { const y = g (x) return y } function f3 (x ) { g (x) }
尾递归优化
尾递归优化 (Tail Recursion Optimization,简称 TRO)是编译器或解释器对尾递归函数进行的一种优化处理。它可以在一定条件下减少递归函数调用的开销,从而提高程序的运行效率,并防止因递归深度过大而导致的栈溢出(Stack Overflow)问题 。
注意:
并非所有递归都能转换为尾递归 :只有在函数返回结果完全依赖于递归调用结果时,才能转换为尾递归。
尾递归优化的可移植性 :不同语言和环境对尾递归优化的支持程度不同。在不支持尾递归优化的环境中,过深的递归仍然可能导致栈溢出。
使用累加器 :为了将普通递归转换为尾递归,通常需要引入一个累加器参数 ,用于在每次递归时携带中间结果。
尾递归
尾递归 (Tail Recursion)是递归的一种特殊形式,也是尾调用的一种特殊形式,指的是在一个函数的最后一步是直接返回递归调用的结果,并且不再进行其他操作。换句话说,在尾递归中,递归调用的返回值直接就是函数的返回值 。
尾递归的一般形式:
1 2 3 4 5 6 7 function tailRecursiveFunction (params ) { if (baseCondition) { return baseResult; } else { return tailRecursiveFunction (newParams); } }
尾递归的优势:
节省栈空间 :在尾递归中,当前函数的栈帧在递归调用之后不再需要,因此编译器或解释器可以选择直接复用这个栈帧,从而不会随着递归深度增加而增加栈帧的数量 。
防止栈溢出 :由于尾递归优化后不会增加栈帧的深度,因此即使递归调用次数非常多,也不会导致栈溢出。
尾递归优化的原理:
在常规递归中,每次函数调用都会创建一个新的栈帧来保存当前函数的状态(包括参数、局部变量和返回地址等)。这些栈帧会在函数调用结束时逐一弹出。
而在尾递归 中,由于递归调用是函数的最后一步,当前栈帧的所有状态在递归调用后都不再需要,因此可以直接复用当前栈帧,而无需创建新的栈帧 。整个过程中只有一个栈桢 。
尾递归与普通递归比较 阶乘计算
普通递归计算阶乘:
1 2 3 4 5 6 7 8 9 function factorial (n ) { if (n === 0 ) { return 1 ; } else { return n * factorial (n - 1 ); } } console .log (factorial (5 ));
逐层递归展开 :普通递归的计算是从 n 逐步递归到 0,每一层都需要等待下一层的结果才能继续计算。
栈空间使用多 :每次递归调用都会创建一个新的栈帧,因此递归的深度等同于栈帧的数量。当 n 较大时,递归深度增加,可能会导致栈溢出。
时间复杂度 :时间复杂度为 $O(n)$,即阶乘的计算时间与 n 成线性关系。
尾递归计算阶乘:
1 2 3 4 5 6 7 8 9 10 11 12 function factorialTailRecursive (n, accumulator = 1 ) { if (n === 0 || n === 1 ) { return accumulator; } else { return factorialTailRecursive (n - 1 , n * accumulator); } } console .log (factorialTailRecursive (5 ));
累积器优化 :尾递归版本通过引入 accumulator
累积器参数,将每一步的计算结果传递到下一次递归调用中,避免了普通递归的层层展开。
栈空间使用少 :如果编译器或解释器支持尾递归优化(TCO),尾递归可以复用栈帧,不会随着递归深度增加而增加栈的使用,从而避免栈溢出。
时间复杂度 :与普通递归一样,尾递归的时间复杂度也是 $O(n)$,但由于尾递归优化,实际执行效率更高,尤其在支持 TCO 的环境中。
循环迭代实现:
1 2 3 4 5 6 7 8 9 function factorialIterative (n ) { let result = 1 ; for (let i = 1 ; i <= n; i++) { result *= i; } return result; } console .log (factorialIterative (5 ));
斐波那契数列
普通递归:
1 2 3 4 5 6 7 8 9 10 11 function fibonacci (n ) { if (n === 0 ) { return 0 ; } else if (n === 1 ) { return 1 ; } else { return fibonacci (n - 1 ) + fibonacci (n - 2 ); } } console .log (fibonacci (10 ));
重复计算 :每次计算 $F(n)$ 时,都需要重新计算 $F(n - 1)$ 和 $F(n - 2)$,导致大量重复计算。例如,计算 $F(10)$ 时,fibonacci(9)
和 fibonacci(8)
会被多次计算。
时间复杂度高 :由于存在大量的重复计算,普通递归的时间复杂度是指数级别的 $O(2^n)$,当 n 很大时,效率会非常低。
栈空间使用大 :每次递归调用都会创建新的栈帧,当 n 较大时,递归深度增加,可能会导致栈溢出。
尾递归实现:
1 2 3 4 5 6 7 8 9 10 11 function fibonacciTailRecursive (n, a = 0 , b = 1 ) { if (n === 0 ) { return a; } else if (n === 1 ) { return b; } else { return fibonacciTailRecursive (n - 1 , b, a + b); } } console .log (fibonacciTailRecursive (10 ));
无重复计算 :尾递归利用额外的参数 a
和 b
来保存计算的中间结果,避免了重复计算。
时间复杂度低 :尾递归的时间复杂度是线性级别的 $O(n)$,每一步计算都仅依赖于前一步的结果,因此效率较高。
栈空间使用少 :如果编译器或解释器支持尾递归优化(TCO),尾递归可以复用栈帧,不会随着递归深度增加而增加栈的使用,从而避免栈溢出。
循环迭代实现:
1 2 3 4 5 6 7 8 9 function fibonacciIterative (n ) { let a = 0 , b = 1 ; for (let i = 0 ; i < n; i++) { [a, b] = [b, a + b]; } return a; } console .log (fibonacciIterative (10 ));
动态规划实现: 动态规划使用自底向上的方法,通过一个数组来存储计算结果,避免了递归调用和重复计算 。
1 2 3 4 5 6 7 8 9 10 11 12 function fibonacciDP (n ) { if (n === 0 ) return 0 ; if (n === 1 ) return 1 ; let fib = [0 , 1 ]; for (let i = 2 ; i <= n; i++) { fib[i] = fib[i - 1 ] + fib[i - 2 ]; } return fib[n]; } console .log (fibonacciDP (10 ));
尾递归优化的支持
ECMAScript 2015(即 ES6)标准首次引入了对尾调用优化的要求。然而,实际情况是,许多 JavaScript 引擎并没有完全实现这一特性。
V8 引擎(Chrome、Node.js) :V8 引擎在早期版本中实现了一些尾调用优化,但后来为了兼容性等问题,取消了这一优化特性。因此,现代的 Chrome 浏览器和 Node.js 中通常不支持尾递归优化。
SpiderMonkey 引擎(Firefox) :同样地,Firefox 的 JavaScript 引擎在早期也尝试支持尾递归优化,但由于兼容性和性能的考虑,这一特性未能被广泛采用。
JavaScriptCore 引擎(Safari) :Safari 的引擎 JavaScriptCore 在某些版本中支持尾递归优化,但整体支持不稳定。
支持尾递归优化的语言或环境,通常会在检测到尾递归时执行如下优化:
复用栈帧 :在尾递归调用时,复用当前栈帧,避免栈帧的增长。
跳转而非调用 :在尾递归调用时,进行跳转而不是函数调用,从而降低函数调用的开销。
惰性函数
惰性函数 (Lazy Function)是一种设计模式,旨在延迟计算,直到函数的结果确实需要使用时才执行计算 。惰性函数的一个常见应用场景是优化程序性能,尤其是在某些计算代价较高或结果不总是需要的情况下。惰性函数可以通过在第一次调用时计算结果,并将该结果缓存起来,以避免后续的重复计算。
第一次调用 :执行必要的计算,并将结果存储或缓存起来。
后续调用 :直接返回已经计算并存储的结果,而不再重复计算。
惰性函数的实现 普通实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function lazyFunction ( ) { let result; function compute ( ) { console .log ('Computing...' ); return 42 ; } return function ( ) { if (result === undefined ) { result = compute (); } return result; }; } const getLazyValue = lazyFunction ();console .log (getLazyValue ()); console .log (getLazyValue ());
闭包实现 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 function lazyValue (fn ) { let computed = false ; let value; return function ( ) { if (!computed) { value = fn (); computed = true ; } return value; }; } const expensiveCalculation = ( ) => { console .log ('Performing expensive calculation...' ); return 99 * 99 ; }; const getLazyResult = lazyValue (expensiveCalculation);console .log (getLazyResult ()); console .log (getLazyResult ());
惰性函数的应用 惰性函数初始化模式 函数在第一次调用时初始化其行为,并通过重写函数来保持后续调用的一致性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function lazyInitialization ( ) { console .log ('Initial computation...' ); lazyInitialization = function ( ) { console .log ('Already initialized.' ); }; return 'Initialized Value' ; } console .log (lazyInitialization ()); console .log (lazyInitialization ());
惰性函数与模块模式 惰性函数与模块模式结合,延迟模块的初始化或加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const LazyModule = (function ( ) { let initialized = false ; function initialize ( ) { console .log ('Initializing module...' ); initialized = true ; } return { doSomething : function ( ) { if (!initialized) { initialize (); } console .log ('Doing something...' ); } }; })(); LazyModule .doSomething (); LazyModule .doSomething ();
函子
在数学上,函子是一种映射,它将一个范畴中的对象和态射(morphism)映射到另一个范畴中,同时保持范畴结构不变。
在编程中,函子是一种封装了值和操作的类型 ,它允许我们对值进行变换,而不解包(unwrap)这个值。
函子可以用来管理值和值的变化过程,把异常和异步操作等副作用控制在可控的范围之内。
函子是为了解决纯函数中的异常带来的副作用,函子可以帮我们控制副作用(IO),进行异常处理(Either)或异步任务(Task)
Container
如果一个对象内部持有一个值,包含值和值的变形关系(这个变形关系就是函数) ,则称之为容器Container。
1 2 3 4 5 6 7 class Container { constructor (value ) { this .value = value } } const container = new Container (1 )console .log (container.value )
Functor
函子 可以简单地理解为一个实现了 map
函数(映射)的容器。它接受一个容器(通常是一个对象或者结构),并提供了一种将函数作用于容器内数据的方式,而不改变容器的结构。
在编程中,函子通常表现为一个对象,它包含了一个值,并且提供了一个 map
方法,map
方法 将一个函数应用于这个值并返回一个新的函子,也就是将函子映射成一个新的函子 。其形式如下:
1 2 3 4 5 6 7 8 9 10 11 class Functor { constructor (value ) { this .value = value } map (fn ) { return new Functor (fn (this .value )) } } console .log (new Functor (5 ).map ((x ) => x + 1 ).map ((x ) => x * x))
函数式编程的运算不直接操作值,而是由函子完成 函子就是个实现了 map
方法的对象 我们可以把函子想象成一个盒子,这个盒子里封装了一个值,想要处理盒子中的值,我们需要给盒子的 map
方法传递一个处理值的函数(纯函数) ,由这个函数来对值进行处理 最终map
方法返回一个包含新值的盒子(函子)
数组(Array)是一个天然的函子,因为数组本身就实现了 map
方法 :
1 2 3 const arr = [1 , 2 , 3 ];const newArr = arr.map (x => x * 2 );console .log (newArr);
函子的特性
保持结构不变 : 当我们对函子的值应用 map
时,函子的结构不会改变。map
方法只会变换函子内的值,并返回一个同类型的新函子。
1 2 3 const functor = new Functor (10 );const newFunctor = functor.map (x => x * 2 );console .log (newFunctor instanceof Functor );
保持可组合性(结合律) : 函子必须保持可组合性,这意味着我们可以将多个 map
操作链式组合起来,而不改变结果。
1 2 3 4 5 6 7 8 const double = x => x * 2 ;const increment = x => x + 1 ;const result1 = new Functor (2 ).map (double).map (increment);const result2 = new Functor (2 ).map (x => increment (double (x)));console .log (result1.value ); console .log (result2.value );
Pointed函子
Pointed 函子是实现了of
静态方法的函子 of
方法是为了避免使用 new
来创建对象,更深层的含义是 of
方法用来把值放到上下文Context (把值放到容器中,使用map
来处理值)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class PointedContainer { static of (value ) { return new PointedContainer (value) } constructor (value ) { this ._value = value } map (fn ) { return PointedContainer .of (fn (this ._value )) } } const pointedContainer = PointedContainer .of (1 )console .log (pointedContainer)console .log (pointedContainer.map (value => value + 1 ))
MayBe函子
Maybe
用于处理可能为 null
或 undefined
的值。它通常有两个状态:
Just(value)
表示存在的值。
Nothing
表示不存在的值。
Maybe
函子使我们能够安全地处理可能不存在的值,而无需显式检查 null
或 undefined
。
Maybe
函子的 map
方法只在 Just
的情况下应用函数,而在 Nothing
的情况下返回 Nothing
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Maybe { constructor (value ) { this .value = value; } static just (value ) { return new Maybe (value); } static nothing ( ) { return new Maybe (null ); } map (fn ) { return this .value !== null ? Maybe .just (fn (this .value )) : Maybe .nothing (); } } const maybeValue = Maybe .just (5 ).map (x => x * 2 );console .log (maybeValue.value ); const maybeNothing = Maybe .nothing ().map (x => x * 2 );console .log (maybeNothing.value );
Either函子
Either
函子通常用于表示一种可能有两种状态的值:
Right(value)
:表示成功状态,值存在。
Left(value)
:表示失败状态,包含错误信息。
Either
函子用于处理可能成功或失败的计算,通常用于错误处理;也可以用来处理默认值。
map
方法通常只在 Right
状态下应用函数,而在 Left
状态下保持不变。
右边有值就用右边的,没有就用左边的
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 class Either { constructor (left, right ) { this .left = left this .right = right } static right (value ) { return new Either (null , value) } static left (value ) { return new Either (value, null ) } map (fn ) { return this .right !== null ? Either .right (fn (this .right )) : Either .left (this .left ) } get value () { return this .right || this .left } } const eitherSuccess = Either .right (10 ).map (x => x * 2 );console .log (eitherSuccess.left ); console .log (eitherSuccess.right ); console .log (eitherSuccess.value ); const eitherError = Either .left ('Error occurred' ).map (x => x * 2 );console .log (eitherError.left ); console .log (eitherError.right ); console .log (eitherError.value );
AP函子
AP函子 (Applicative Functor,简称为AP
), 是一种实现了 ap
方法的函子, 允许我们将包含在容器中的函数应用于另一个容器中的值。
ap
方法可以让一个函子内的函数使用另一个函子的值进行计算。 ap
方法的参数不是函数,而是另一个函子 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Ap { static of (value ) { return new Ap (value) } constructor (value ) { this .value = value } ap (functor ) { return Ap .of (this .value (functor.value )) } } const A = new Ap (x => x + 1 )const B = new Ap (2 )const result = A.ap (B)console .log (result.value )
Monad函子——单子 函子的值也可以是函子,这样会出现多层函子嵌套的情况。
Monad(单子 【不可分割的实体】)函子的作用是,总是返回一个单层的函子 。
单子 (Monad)不仅是一个函子(Functor),也是一种设计模式,用于处理计算中的副作用(如状态、I/O、异常处理等),并以一种结构化的方式将一系列操作组合起来。
Monad 可以看作是Applicative Functor(Ap函子)的进一步扩展,它不仅允许我们对容器内的值进行操作,还允许我们将多个操作组合在一起。
单子在保证代码纯净性的同时,允许处理那些通常在纯函数式编程中难以处理的任务。
Monad有两个核心方法:
of
(或 unit
/ return
) : 该方法将一个值放入容器中,创建一个Monad实例。
flatMap
(或 bind
/ chain
) : 该方法允许我们将一个返回Monad的函数应用于一个已有的Monad,避免嵌套的容器。
单子有一个flatMp
方法,与map
方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值 ,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
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 class Monad { constructor (value ) { this .value = value } static of (value ) { return new Monad (value) } map (fn ) { return Monad .of (fn (this .value )) } join ( ) { return this .value } flatMap (fn ) { return this .map (fn).join () } } const result = Monad .of ('a' ) .flatMap (str => Monad .of (str.toUpperCase ())) .flatMap (str => Monad .of (str + 'b' )) .flatMap (str => Monad .of (str + 'c' )) console .log (result) console .log (result.join ())
IO函子
IO函子 是函数式编程中的一种特殊函子,用于处理带有副作用的操作(如读写文件、打印日志、获取当前时间、处理用户输入等) 。
在纯函数式编程中,函数应该是纯净的,即相同的输入总是产生相同的输出,且没有副作用。而现实中,程序通常需要与外部世界交互(这往往带来副作用)。IO函子通过 将副作用延迟到真正执行时才发生 ,帮助我们保持函数的纯净性 。
通过使用IO函子,我们不会立即执行副作用操作,而是将操作封装在函子内部,延迟到需要时再执行。
IO函子特点 IO函子有以下几个重要的特点:
延迟执行 :IO函子内部封装的副作用操作不会立即执行,而是被延迟到明确调用 run
方法时才执行。
保持纯函数 :通过将副作用操作封装在IO函子中,我们可以保持大多数代码的纯净性,使得函数式编程的优势(如易测试性、可组合性)得以保留。
可组合性 :使用 map
和 flatMap
,我们可以将多个IO操作组合成复杂的行为,并通过链式调用保持代码的清晰和简洁。
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 class IO { constructor (effect ) { if (typeof effect !== 'function' ) { throw new Error ('IO Usage: function required' ) } this .effect = effect } static of (value ) { return new IO (() => value) } map (fn ) { return new IO (() => fn (this .effect ())) } flatMap (fn ) { return new IO (() => fn (this .effect ()).run ()) } run ( ) { return this .effect () } }
示例
1 2 3 4 5 const print = new IO (() => console .log ('Hello, World!' ));print.run ();
1 2 3 4 5 6 7 8 9 10 const read = new IO (() => 'hello, world' );const write = (text ) => new IO (() => console .log (text));const readAndWrite = read.flatMap ((text ) => write (text));readAndWrite.run ();
应用
处理IO操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const readFile = filename => new IO (() => { const fs = require ('fs' ); return fs.readFileSync (filename, 'utf-8' ); }); const printContent = content => new IO (() => { console .log (content); }); const readAndPrint = filename => readFile (filename).flatMap (printContent); readAndPrint ('test.txt' ).run ();
处理异步操作
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 42 class AsyncIO extends IO { flatMap (fn ) { return new AsyncIO (() => { return this .effect ().then (result => fn (result).run ()); }); } run ( ) { return this .effect (); } } const fetchData = url => new AsyncIO (() => fetch (url).then (response => response.json ())); const logData = data => new AsyncIO (() => { console .log (data); return data; }); const fetchAndLog = url => fetchData (url).flatMap (logData); fetchAndLog ('https://jsonplaceholder.typicode.com/posts/1' ).run ().then (() => { console .log ('Data fetched and logged successfully' ); });
Task函子(异步执行)
与传统的 Promise 类似,Task
函子可以封装异步操作并支持链式操作 ,但它更符合函数式编程的思想,避免了副作用,推迟了执行,直到明确地要求执行 。
特点
惰性求值 :Task
函子不会立即执行封装的异步操作。与 Promise 不同的是,Task
只在你明确地调用时(通常通过 fork
方法)才会执行。这种惰性求值使得 Task
更容易组合和处理。
不变性 :一旦创建,Task
的内容不会改变。所有的链式操作(如 map
和 flatMap
)都会返回新的 Task
函子,而不会修改原来的 Task
。
强大的组合性 :Task
可以与其他函子进行组合,也可以通过 map
、flatMap
等方法对异步操作进行组合,形成复杂的异步处理流程。
实现 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 class Task { constructor (fork ) { this .fork = fork } map (fn ) { return new Task ((reject, resolve ) => this .fork (reject, (result ) => resolve (fn (result))), ) } flatMap (fn ) { return new Task ((reject, resolve ) => this .fork (reject, (result ) => fn (result).fork (reject, resolve)), ) } static of (value ) { return new Task ((_, resolve ) => resolve (value)) } fork (reject, resolve ) { return this .fork (reject, resolve) } }
示例 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 const fetchTask = (url ) => new Task ((reject, resolve ) => { fetch (url) .then ((response ) => response.json ()) .then (resolve) .catch (reject) }) const logTask = (data ) => new Task ((_, resolve ) => { console .log (data) resolve (data) }) const fetchAndLogTask = (url ) => fetchTask (url).flatMap (logTask)fetchAndLogTask ('https://jsonplaceholder.typicode.com/posts/1' ).fork ( (err ) => console .error (err), () => console .log ('Task completed successfully' ), )
fetchTask
:封装了一个异步 fetch
请求,返回 Task
函子。该函子可以通过 fork
方法来处理请求的成功或失败。
logTask
:创建一个任务来打印数据。注意,这里没有立即执行 console.log
,而是通过 Task
的惰性求值,在合适的时机执行。
fetchAndLogTask
:组合了 fetchTask
和 logTask
,使用 flatMap
将数据从 fetchTask
传递到 logTask
中。
fork
方法 :在调用 fetchAndLogTask(url)
后,通过 fork
方法明确地执行整个异步任务链。fork
接受两个回调函数:一个用于处理失败,一个用于处理成功。
Task
vs. Promise
惰性 :Task
不会立即执行,只有在调用 fork
时才执行。Promise
则在创建时就立即执行。
可组合性 :Task
可以通过 flatMap
等方法组合多个异步操作,构建更复杂的流程,而 Promise
的组合性相对较弱。