核心思想
函数式编程将计算视为数学函数的求值,强调:函数是一等公民、避免副作用、数据不可变、用函数组合构建程序。
一等公民的函数
函数可以赋值给变量、作为参数传递、作为返回值。不需要多余的包装层:
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"]