函数式编程不是一种新的语法,而是一种思考方式。它让我们用更简洁、更可预测、更可测试的方式编写代码。理解这些概念,将彻底改变我们编写 JavaScript 的方式。
前言:从命令式到声明式的转变
命令式编程:关注"怎么做"
javascript
const numbers = [1, 2, 3, 4, 5];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
console.log(doubled); // [2, 4, 6, 8, 10]
函数式编程:关注"做什么"
javascript
const numbers2 = [1, 2, 3, 4, 5];
const doubled2 = numbers2.map(n => n * 2);
console.log(doubled2); // [2, 4, 6, 8, 10]
立即调用函数表达式(IIFE)
IIFE 的基本概念
IIFE(Immediately Invoked Function Expression)是定义后立即执行的函数表达式。
IIFE的基本语法
语法1:括号包裹整个调用
javascript
(function() {
console.log('括号包裹整个调用');
}());
语法2:括号包裹函数,然后调用
javascript
(function() {
console.log('括号包裹函数,然后调用');
})();
注:以上两种写法只是两种不同的风格,它们在功能上完全等价,没有实质性区别。
语法对比:函数声明 vs 函数表达式 vs IIFE
1. 函数声明 - 不会立即执行
javascript
function greet() {
console.log('Hello!');
}
greet(); // 需要显式调用
2. 函数表达式 - 也不会立即执行
javascript
const greetExpr = function() {
console.log('Hello from expression!');
};
greetExpr(); // 需要显式调用
3. IIFE - 定义后立即执行
javascript
(function() {
console.log('Hello from IIFE!'); // 立即执行
})();
IIFE操作符
在 JavaScript 中,以 function 开头的语句会被解析为函数声明,而函数声明不能直接跟 () 执行,因此出现了操作符。添加这些操作符后,JavaScript 引擎会将 function... 解析为函数表达式,这样就可以立即执行了。
逻辑非运算符!
javascript
const result = !function () {
console.log('逻辑非');
}();
console.log(result); // true
上述代码会将立即执行函数的返回值进行取反,由于上述函数没有明确返回值,故默认返回 undefined , !undefined 结果为 true,因此 result 的值为 true 。
一元加运算符+
javascript
const result = +function () {
console.log('一元加');
}();
console.log(result); // NaN
上述代码立即执行函数的返回值转换为数字,由于上述函数没有明确返回值,故默认返回 undefined , undefined 转为为数字结果为 NaN,因此 result 的值为 NaN 。
void 运算符
javascript
const result = void function () {
console.log('void');
}();
console.log(result); // undefined
上述代码中,立即执行函数的返回值永远为 undefined ,这是 void 关键字的特性使然。
IIFE 的实际应用
应用1:创建私有作用域
使用IIFE可以创建模块作用域,模块作用域内变量在IIFE外部无法直接访问,即为私有作用域。在IIFE内部,我们可以提供公共方法,去访问这些私有作用域:
javascript
(function () {
// 这些变量在IIFE外部无法访问
var privateVar = '我是私有的';
var secret = 42;
// 提供公共方法访问私有变量
myModule = {
getSecret: function () {
return secret;
},
publicMethod: function () {
console.log('公共方法可以访问私有变量:', privateVar);
}
};
})();
console.log(myModule.getSecret()); // 42
myModule.publicMethod(); // "公共方法可以访问私有变量: 我是私有的"
console.log(privateVar); // ReferenceError: privateVar is not defined
console.log(secret); // ReferenceError: secret is not defined
应用2:避免变量冲突
假设有多个第三方库,它们都使用了同一个变量,如 jQuery 和 Prototype.js ,它们都用了 $ 符号,直接使用 $ 符号就会冲突。这种情况下,我们就可以采用 IIFE 的方式,将 $ 保护起来:
javascript
(function($) {
// 在这个作用域内,$就是jQuery
$(document).ready(function() {
console.log('jQuery准备好了');
});
})(jQuery); // 传入jQuery对象
// Prototype.js的$不受影响
IIFE 的现代替代方案
方案1:ES Module(最佳方案)
javascript
// module.js
const privateVar = '私有变量';
export const publicVar = '公共变量';
export function publicMethod() {
return privateVar;
}
// main.js
import { publicVar, publicMethod } from './module.js';
方案2:块级作用域 + 闭包
javascript
{
const privateData = '块级私有数据';
let counter = 0;
counterModule = {
increment: () => ++counter,
getValue: () => counter
};
}
console.log(counterModule.increment()); // 1
console.log(counterModule.getValue()); // 1
// console.log(privateData); // ReferenceError
// console.log(counter); // ReferenceError
方案3:类与私有字段
javascript
class SecureModule {
#secret = '绝密信息';
#counter = 0;
getSecret() {
return this.#secret;
}
increment() {
return ++this.#counter;
}
}
const module = new SecureModule();
console.log(module.getSecret()); // "绝密信息"
console.log(module.increment()); // 1
// console.log(module.#secret); // SyntaxError
纯函数(Pure Functions)
什么是纯函数?
纯函数是函数式编程的基石,它具有两个核心特征:
- 相同的输入,总是得到相同的输出
- 没有副作用
纯函数 vs 非纯函数
纯函数示例
javascript
function add(a, b) {
return a + b;
}
非纯函数示例
javascript
let counter = 0;
function increment() {
counter++; // 修改外部状态
return counter;
}
纯函数的优势
优势1:可预测性
纯函数中对于相同的输入,总是得到相同的输出,因此其结果是可以预测的:
javascript
const calculatePrice = (price, taxRate) => price * (1 + taxRate);
console.log('价格计算: $100 * 1.1 =', calculatePrice(100, 0.1)); // 110
console.log('再次计算: $100 * 1.1 =', calculatePrice(100, 0.1)); // 110(总是相同)
优势2:易于测试
javascript
function testCalculatePrice() {
const result = calculatePrice(100, 0.1);
const expected = 110;
console.log(`测试 ${result === expected ? '通过' : '失败'}: ${result} === ${expected}`);
}
testCalculatePrice();
优势3:引用透明性
javascript
const price1 = calculatePrice(100, 0.1);
const price2 = calculatePrice(100, 0.1);
console.log('price1 === price2:', price1 === price2); // true
console.log('可以直接替换:', calculatePrice(100, 0.1) === 110); // true
优势4:可缓存性
javascript
function square(x) {
console.log(`计算 ${x} 的平方`);
return x * x;
}
// 缓存包装器
function memoize(fn) {
const cache = {};
return function (x) {
if (cache[x] !== undefined) {
console.log(`从缓存获取 ${x} 的平方`);
return cache[x];
}
const result = fn(x);
cache[x] = result;
return result;
};
}
// 使用缓存
const memoizedSquare = memoize(square);
console.log(memoizedSquare(5)); // 第一次计算
console.log(memoizedSquare(5)); // 从缓存获取
常见的副作用及其解决方案
副作用类型1:修改输入参数
javascript
const impureAddToArray = (array, item) => {
array.push(item); // 副作用:修改输入参数
return array;
};
解法方案:返回新数组,不修改原数组
javascript
const pureAddToArray = (array, item) => {
return [...array, item]; // 返回新数组,不修改原数组
};
副作用类型2:修改外部变量
javascript
let globalCount = 0;
const impureIncrement = () => {
globalCount++; // 副作用:修改全局状态
return globalCount;
};
解决方案:返回新值
javascript
const pureIncrement = (count) => {
return count + 1; // 不修改外部状态
};
副作用类型3:I/O操作
javascript
const impureFetchData = (url) => {
// 副作用:网络请求
fetch(url)
.then(response => response.json())
.then(data => console.log('数据:', data));
};
解决方案:返回一个函数,延迟执行副作用
javascript
const pureFetchData = (url) => {
// 返回一个函数,延迟执行副作用
return () => {
return fetch(url)
.then(response => response.json());
};
};
副作用类型4:异常和错误
javascript
const impureParseJSON = (str) => {
return JSON.parse(str); // 可能抛出异常
};
解决方案:异常捕获
javascript
const pureParseJSON = (str) => {
try {
return { success: true, data: JSON.parse(str) };
} catch (error) {
return { success: false, error: error.message };
}
};
高阶函数(Higher-Order Functions)
什么是高阶函数?
高阶函数 是指能够接受函数作为参数,或者返回函数作为结果的函数:
接受函数作为参数
javascript
const greet = (name, formatter) => {
return formatter(name);
};
const shout = (name) => `${name.toUpperCase()}!`;
const whisper = (name) => `psst... ${name}...`;
console.log(greet('zhangsan', shout)); // "ZHANGSAN!"
console.log(greet('lisi', whisper)); // "psst... lisi..."
返回函数作为结果
javascript
const multiplier = (factor) => {
return (number) => number * factor;
};
const double = multiplier(2);
const triple = multiplier(3);
console.log('double(5):', double(5)); // 10
console.log('triple(5):', triple(5)); // 15
同时接受和返回函数
javascript
const compose = (f, g) => {
return (x) => f(g(x));
};
const addOne = (x) => x + 1;
const square = (x) => x * x;
const addOneThenSquare = compose(square, addOne);
console.log('addOneThenSquare(2):', addOneThenSquare(2));
柯里化(Currying)
什么是柯里化?
柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
javascript
// 原始函数(多参数)
const addThreeNumbers = (a, b, c) => a + b + c;
console.log('原始函数:', addThreeNumbers(1, 2, 3)); // 6
// 柯里化版本
const curriedAdd = (a) => {
return (b) => {
return (c) => {
return a + b + c;
};
};
};
console.log('柯里化版本:', curriedAdd(1)(2)(3)); // 6
柯里化的优势
1. 参数复用
javascript
const addFive = curriedAdd(5);
console.log('addFive(10)(15):', addFive(10)(15)); // 30
2. 延迟计算
javascript
const multiply = (a) => (b) => a * b;
const double = multiply(2);
const triple = multiply(3);
console.log('double(10):', double(10)); // 20
console.log('triple(10):', triple(10)); // 30
3. 函数组合
javascript
const greet = (greeting) => (name) => `${greeting}, ${name}!`;
const sayHello = greet('Hello');
const sayHi = greet('Hi');
console.log(sayHello('zhangsan')); // "Hello, zhangsan!"
console.log(sayHi('lisi')); // "Hi, lisi!"
手动实现柯里化
javascript
const manualCurry = (fn) => {
const arity = fn.length; // 函数期望的参数个数
const curried = (...args) => {
if (args.length >= arity) {
return fn(...args);
} else {
return (...moreArgs) => {
return curried(...args, ...moreArgs);
};
}
};
return curried;
};
函数组合(Function Composition)
什么是函数组合?
函数组合是将多个函数组合成一个新函数的过程,新函数的输出作为下一个函数的输入。
手动组合
javascript
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
// 手动组合:从右向左
const addOneThenDoubleThenSquare = (x) => {
const afterAddOne = addOne(x);
const afterDouble = double(afterAddOne);
const afterSquare = square(afterDouble);
return afterSquare;
};
console.log('手动组合:', addOneThenDoubleThenSquare(2)); // ((2+1)*2)^2 = 36
组合函数
javascript
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
// 组合函数
const compose = (...fns) => (x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
// 从右向左组合:square(double(addOne(x)))
const addOneThenDoubleThenSquare = compose(square, double, addOne);
console.log('函数组合:', addOneThenDoubleThenSquare(2));
管道函数
javascript
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
// 管道函数
const pipe = (...fns) => {
return (initialValue) => {
return fns.reduce((value, fn) => fn(value), initialValue);
};
};
// 从左向右组合:addOne → double → square
const addOneThenDoubleThenSquarePipe = pipe(addOne, double, square);
console.log('管道组合:', addOneThenDoubleThenSquarePipe(2));
Pointfree 风格编程
Pointfree 风格(无参数风格)是一种编程风格,函数定义不显式提及它所操作的数据参数。
非 Pointfree 风格示例
javascript
const nonPointfree = (users) => {
return users
.filter(user => user.age >= 18)
.map(user => user.name)
.map(name => name.toUpperCase());
};
Pointfree 风格
javascript
const isAdult = user => user.age >= 18;
const getName = user => user.name;
const toUpperCase = str => str.toUpperCase();
const getAdultUserNames = (users) => {
return users
.filter(isAdult)
.map(getName)
.map(toUpperCase);
};
现代 JavaScript 中的函数式特性
1. 箭头函数
javascript
const add = (a, b) => a + b;
2. 解构与剩余参数
javascript
const processArgs = (first, second, ...rest) => {
console.log('前两个:', first, second);
console.log('其余:', rest);
};
3. 默认参数
javascript
const greet = (name, greeting = 'Hello') => `${greeting}, ${name}!`;
4. 数组和对象的扩展运算
javascript
const combine = (...arrays) => [].concat(...arrays);
const merge = (...objects) => Object.assign({}, ...objects);
5. Promise 和 async/await
javascript
const asyncPipe = (...fns) => async (initial) => {
return fns.reduce(async (value, fn) => {
const resolvedValue = await value;
return fn(resolvedValue);
}, Promise.resolve(initial));
};
6. 新的数组方法
javascript
const numbers = [1, 2, 3, 4, 5];
const flatMapped = numbers.flatMap(x => [x, x * 2]);
7. 可选链和空值合并
javascript
const safeGet = (obj, path) => {
return path.split('.').reduce(
(acc, key) => acc?.[key] ?? null,
obj
);
};
结语
函数式编程提供了一套强大的工具和思维方式,通过掌握这些核心概念,我们能够编写出更简洁、更可维护、更可测试的代码。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!