Skip to content
imshengli blog
Go back

JavaScript 函数式编程:纯函数、柯里化与组合

JavaScript 函数式编程核心概念:纯函数、不可变性、高阶函数、柯里化、函数组合与实际应用。

· 6 min

核心思想

函数式编程将计算视为数学函数的求值,强调:函数是一等公民、避免副作用、数据不可变、用函数组合构建程序。

一等公民的函数

函数可以赋值给变量、作为参数传递、作为返回值。不需要多余的包装层:

var hi = function(name) {
  return "Hi " + name;
};

// 不需要这样包一层
var greeting = function(name) {
  return hi(name);
};
// 直接赋值即可
var greeting = hi;

多余的包装层在函数签名变化时会造成维护负担:

// 如果回调签名变了(加了 err 参数),包装层必须跟着改
httpGet('/post/2', function(json) {
  return renderPost(json);
});
// 直接传引用则自动适配
httpGet('/post/2', renderPost);

命名时保持通用,不要绑定到特定业务数据上:

// 耦合到 articles
var validArticles = function(articles) {
  return articles.filter(function(article) {
    return article !== null && article !== undefined;
  });
};

// 通用版本,可复用
var compact = function(xs) {
  return xs.filter(function(x) {
    return x !== null && x !== undefined;
  });
};

纯函数

相同的输入永远得到相同的输出,且没有副作用。

// 不纯:依赖外部变量
var minimum = 21;
var checkAge = function(age) {
  return age >= minimum;
};

// 纯函数:所有依赖通过参数传入
var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};

纯函数的好处:

可缓存性:相同输入总是相同输出,结果可以缓存。

var memoize = function(f) {
  var cache = {};
  return function() {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};

var squareNumber = memoize(function(x) { return x * x; });
squareNumber(4); //=> 16
squareNumber(4); //=> 16,从缓存读取

可测试性:不需要 mock 环境,直接断言输入输出。

引用透明:表达式可以用其结果替换而不改变程序行为。

不可变性

不修改原数据,而是返回新数据:

// 可变方式(不推荐)
var addItem = function(arr, item) {
  arr.push(item);
  return arr;
};

// 不可变方式
var addItem = function(arr, item) {
  return arr.concat([item]);
};

// 对象更新用展开运算符
var updateUser = function(user, name) {
  return { ...user, name: name };
};

好处:避免共享状态导致的意外修改,使数据流向可追踪。

高阶函数

接收函数作为参数或返回函数的函数:

var numbers = [1, 2, 3, 4, 5];

// map:转换每个元素
numbers.map(function(n) { return n * 2; });
// [2, 4, 6, 8, 10]

// filter:筛选元素
numbers.filter(function(n) { return n % 2 === 0; });
// [2, 4]

// reduce:归约为单个值
numbers.reduce(function(acc, n) { return acc + n; }, 0);
// 15

柯里化(Currying)

将多参数函数转换为一系列单参数函数,每次传入一个参数返回新函数:

// 普通函数
var add = function(a, b) { return a + b; };
add(1, 2); // 3

// 柯里化版本
var add = function(a) {
  return function(b) { return a + b; };
};
add(1)(2); // 3

// 先固定部分参数
var increment = add(1);
increment(5);  // 6
increment(10); // 11

通用的柯里化工具:

var curry = function(fn) {
  var arity = fn.length;
  return function curried() {
    var args = Array.prototype.slice.call(arguments);
    if (args.length >= arity) {
      return fn.apply(null, args);
    }
    return function() {
      var moreArgs = Array.prototype.slice.call(arguments);
      return curried.apply(null, args.concat(moreArgs));
    };
  };
};

var multiply = curry(function(a, b, c) { return a * b * c; });
multiply(2)(3)(4);  // 24
multiply(2, 3)(4);  // 24

柯里化在数据处理管道中很有用——预配置参数,生成专用函数:

var filter = curry(function(fn, arr) { return arr.filter(fn); });
var map = curry(function(fn, arr) { return arr.map(fn); });

var filterPositive = filter(function(n) { return n > 0; });
var doubleAll = map(function(n) { return n * 2; });

filterPositive([-1, 0, 2, 3]);  // [2, 3]
doubleAll([1, 2, 3]);           // [2, 4, 6]

函数组合(Compose)

将多个函数串联,前一个函数的输出作为后一个的输入,从右到左执行:

var compose = function(f, g) {
  return function(x) { return f(g(x)); };
};

var toUpperCase = function(str) { return str.toUpperCase(); };
var exclaim = function(str) { return str + '!'; };

var shout = compose(exclaim, toUpperCase);
shout('hello'); // "HELLO!"

支持多个函数的 compose:

var compose = function() {
  var fns = Array.prototype.slice.call(arguments);
  return function(x) {
    return fns.reduceRight(function(acc, fn) { return fn(acc); }, x);
  };
};

var head = function(arr) { return arr[0]; };
var reverse = function(arr) { return arr.slice().reverse(); };
var toUpper = function(str) { return str.toUpperCase(); };

var lastUpper = compose(toUpper, head, reverse);
lastUpper(['a', 'b', 'c']); // "C"

与 compose 方向相反的是 pipe(从左到右):

var pipe = function() {
  var fns = Array.prototype.slice.call(arguments);
  return function(x) {
    return fns.reduce(function(acc, fn) { return fn(acc); }, x);
  };
};

var processName = pipe(
  function(s) { return s.trim(); },
  function(s) { return s.toLowerCase(); },
  function(s) { return s.replace(/\s+/g, '-'); }
);
processName('  Hello World  '); // "hello-world"

实际应用

将上述概念组合起来处理数据:

var prop = curry(function(key, obj) { return obj[key]; });
var filter = curry(function(fn, arr) { return arr.filter(fn); });
var map = curry(function(fn, arr) { return arr.map(fn); });
var sortBy = curry(function(fn, arr) {
  return arr.slice().sort(function(a, b) { return fn(a) - fn(b); });
});

var users = [
  { name: 'Alice', age: 30, active: true },
  { name: 'Bob', age: 25, active: false },
  { name: 'Carol', age: 28, active: true }
];

// 筛选活跃用户,按年龄排序,提取名字
var getActiveNames = pipe(
  filter(prop('active')),
  sortBy(prop('age')),
  map(prop('name'))
);

getActiveNames(users); // ["Carol", "Alice"]

参考文档


Share this post on:

Previous Post
JavaScript 面向对象:构造函数、原型链与四种继承
Next Post
JavaScript 字符串:常用方法、模板字符串与 Unicode