JavaScript 函数式编程核心概念

函数式编程不是一种新的语法,而是一种思考方式。它让我们用更简洁、更可预测、更可测试的方式编写代码。理解这些概念,将彻底改变我们编写 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 

上述代码立即执行函数的返回值转换为数字,由于上述函数没有明确返回值,故默认返回 undefinedundefined 转为为数字结果为 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
  );
};

结语

函数式编程提供了一套强大的工具和思维方式,通过掌握这些核心概念,我们能够编写出更简洁、更可维护、更可测试的代码。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
mCell6 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell7 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭7 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清7 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
萧曵 丶7 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
银烛木7 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076607 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声7 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易7 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得07 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化