持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情
函数式编程
和 Lisp、Haskell 不同,JavaScript 并非函数式编程语言,但在 JavaScript 中可以像操纵对象一样操纵函数,也就说在 JavaScript 中应用函数式编程技术。ECMAScript 5 中的数组方法(诸如 map()和 reduce())就可以非常适合用于函数式编程风格。
函数式编程(Function Programming,FP),FP是编程范式之一,我们常说的编程规范还有面向过程编程、面向对象编程。
- 面向过程编程就是按照过程实现我们想要的功能。
- 面向对象编程就是把现实中的对象抽离成程序中的类和对象,通过封装、继承、多态来演示事物时间的联系。
- 函数式编程是把事物和事物之间的联系抽象到程序世界中(有点抽象,在解释一下!)
通过输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数(就是对运算过程中进行抽象,这就是很面向对象有巨大差别的地方,思维模式上的区别) 函数式编程中的函数指的不是程序中的函数,而是运算中的映射关系 相同的输入数据得到相同的输入结果(纯函数)
- 一句话:JavaScript 不是函数式编程语言,但是应用了函数式编程技术。
使用函数处理数组
一个数组元素全是数字,求这些元素的平均值和标准差。
- 使用非函数式编程风格
var data = [1, 1, 3, 5, 5] // 这里是待处理的数组
// 平均数是所有元素的累加和值除以元素个数
var total = 0;
for (var i = 0; i < data.length; i++) total += data[i]
var mean = total / data.length; // 平均数是3
// 计算标准差,首先计算每个数据减去平均数之后偏差的平方然后求和
total = 0;
for (var i = 0; i < data.length; i++) {
var deviation = data[i] - mean;
total += deviation * deviation;
}
var stddev = Math.sqrt(total / (data.length - 1)); // 标准差的值是2
- 可以使用数组方法map(和reduce()来实现同样的计算
// 首先定义两个简单的函数
var sum = function (x, y) {
return x + y
}
var square = function (x) {
return x * x
}
// 然后将这些函数和数组方法配合使用计算出平均数和标准差
var data = [1, 1, 3, 5, 5];
var mean = data.reduce(sum) / data.length;
var deviations = data.map(function (x) {
return x - mean;
});
var stddev = Math.sqrt(deviations.map(square).reduce(sum) / (data.length - 1))
如果我们基于ECMAScript 3来如何实现呢?因为ECMAScript 3中并不包含这些数组方法,如果不存在内置方法的话我们可以自定义map()和reduce()函数
- 自定义map()和reduce()函数
//对于每个数组元素调用函数f(),并返回一个结果数组
//如果Array.prototype.map定义了的话,就使用这个方法
var map = Array.prototype.map ?
function (a, f) {
return a.map(f);
} // 如果已经存在map()方法,就直接使用它
:
function (a, f) { //否则,自己实现一个
var results = [];
for (var i = 0, len = a.length; i < len; i++) {
if (i in a) results[i] = f.call(null, a[i], i, a);
}
return results;
};
// 使用函数f()和可选的初始值将数组a减至一个值
// 如果Array.prototype.reduce存在的话,就使用这个方法
var reduce = Array.prototype.reduce ?
function (a, f, initial) {
if (arguments.length > 2) return a.reduce(f, initial) // 如果传入了一个初始值
else return a.reduce(f); // 否则没有初始值
} :
function (a, f, initial) { // 这个算法来自ES5规范
var i = 0,
len = a.length,
accumulator;
// 以特定的初始值开始,否则第一个值取自a
if (arguments.length > 2) accumulator = initial;
else {
// 找到数组中第一个已定义的索引
if (len === 0) throw TypeError();
while (i < len) {
if (i < len) {
if (i in a) {
accumulator = a[i++];
break;
}
} else i++;
}
if (i === len) throw TypeError();
}
// 对于数组中剩下的元素依次调用f()
while (i < len) {
if (i in a) {
accumulator = f.call(undefined, accumulator, a[i], i, a);
i++;
}
}
return accumulator;
}
// 使用定义的map()和reduce()函数,计算平均值和标准差的代码看起来像这样
var data = [1, 1, 3, 5, 5];
var sum = function (x, y) {
return x + y;
};
var square = function (x) {
return x;
};
var mean = reduce(data, sum) / data.length;
var deviations = map(data, function (x) {
return x - mean;
});
var stddev = Math.sqrt(reduce(map(deviations, square), sum) / data.length - 1);
高阶函数
所谓高阶函数(higher-order function)就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数,来看这个例子:
- 判断奇数
// 这个高阶函数返回一个新的函数,这个新函数将它的实参传人f()
// 并返回f的返回值的逻辑非
function not(f) {
return function () { // 返回一个新的函数
var result = f.apply(this, arguments); // 调用f()
return !result; // 对结果求反
}
}
var even = function (x) { // 判断a是否为偶数的函数
return x % 2;
};
var odd = not(even); // 一个新函数,所做的事情和even()相反
[1, 1, 3, 5, 5].every(odd); // =>true每个元素都是奇数
- 封装map()
// 所返回的函数的参数应当是一个实参数组,并对每个数组元素执行函数f()
// 并返回所有计算结果组成的数组
// 可以对比一下这个函数和上文提到的map()函数
//对于每个数组元素调用函数f(),并返回一个结果数组
//如果Array.prototype.map定义了的话,就使用这个方法
var map = Array.prototype.map ?
function (a, f) {
return a.map(f);
} // 如果已经存在map()方法,就直接使用它
:
function (a, f) { //否则,自己实现一个
var results = [];
for (var i = 0, len = a.length; i < len; i++) {
if (i in a) results[i] = f.call(null, a[i], i, a);
}
return results;
};
function mapper(f) {
return function (a) {
return map(a, f);
};
}
var increment = function (x) {
return x + 1;
};
var incrementer = mapper(increment);
console.log(incrementer([1, 2, 3])); // => [2,3,4]
- 它接收两个函数f()和g(),并返回一个新的函数用以计算f(g()):
// 这里是一个更常见的例子,它接收两个函数f()和g(),并返回-一个新的函数用以计算f(g()):
// 返回的函数h()将它所有的实参传入g(),然后将g()的返回值传入f()
function compose(f, g) {
return function () {
// 需要给f()传入一个参数,所以使用f()的call()方法
// 需要给g()传入很多参数,所以使用g()的apply()方法
return f.call(this, g.apply(this, arguments));
};
}
var square = function (x) {
return x * x
};
var sum = function (x, y) {
return x + y
};
var squareofsum = compose(square, sum);
console.log(squareofsum(2, 3)); // => 25
- partial()和memoize()函数
函数的部分应用(柯里化函数)
柯里化就是可以将函数参数进行逐个绑定
作者在本节讨论的是一种函数变换技巧,即把一次完整的函数调用拆成多次函数调用,每次传入的实参都是完整实参的一部分,每个拆分开的函数叫做不完全函数(partialfunction),每次函数调用叫做不完全调用(partial application),这种函数变换的特点是每次调用都返回一个函数,直到得到最终运行结果为止,举一个简单的例子,将对函数f(1,2,3,4,5,6)的调用修改为等价的f(1,2)(3,4)(5,6),后者包含三次调用,和每次调用相关的函数就是“不完全函数”。
bind()函数不仅可以将对象绑定在函数身上,还可以将函数参数进行绑定。但是传入bind()函数的实参都是放在传入原始函数的实参列表开始的位置
绑定参数的先后位置
// 传给这个函数的参数会传到左侧
function partialLeft(f, ...outerArgs) {
return function (...innerArgs) { // 返回这个函数
let args = [...outerArgs, ...innerArgs]; // 构建参数列表
return f.apply(this, args); // 然后通过它调用f
}
}
// 传给这个函数的参数会传到右侧
function partialRight(f, ...outerArgs) {
return function (...innerArgs) { // 返回这个函数
let args = [...innerArgs,...outerArgs]; // 构建参数列表
return f.apply(this, args); // 然后通过它调用f
}
}
// 补充外部参数的undefined,多的内层参数添加尾部
function partial(f, ...outerArgs) {
return function (...innerArgs) {
let args = [...outerArgs]; // 外部参数模板的局部模板
let innerIndex = 0; // 下一个是那个内部参数
// 循环遍历args,用内部参数填充undefined的值
for (let i = 0; i < args.length; i++) {
if (args[i] === undefined) args[i] = innerArgs[innerIndex++];
}
// 现在把剩余的内部参数都加进来
args.push(innerArgs.slice(innerIndex));
return f.apply(this, args);
}
}
- 根据上面的函数进行调用
const f = function(x, y, z) {
return x * (y - z);
};
partialLeft(f, 2)(3, 4); // -2
partialRight(f, 2)(3, 4); // 6
partial(f, 2)(3, 4) // -6
函数记忆
将值缓存到函数的本身属性上。叫做函数记忆
需要注意的是,记忆只是一种编程技巧,本质上是牺牲算法的空间复杂度以换取更优的时间复杂度,在客户端JavaScript中代码的执行时间复杂度往往成为瓶颈,因此在大多数场景下,这种牺牲空间换取时间的做法以提升程序执行效率的做法是非常可取的
function memoize(f) {
// 返回f()的带有记忆功能的版本
// 只有当f()的实参的字符串表示都不相同时它才会工作
const cache = new Map(); //将值保存在闭包内
return function (...args) {
// 将实参转换为字符串形式,并将其用做缓存的键
let key = args.length + args.join("+");
if (cache.has(key)) {
return cache.get(key);
} else {
let result = f.apply(f, args);
cache.set(key, result);
return result;
}
};
}
memorize()函数创建一个新的对象,这个对象被当做缓存(的宿主)并赋值给一个局部变量,因此对于返回的函数来说它是私有的(在闭包中)。所返回的函数将它的实参数组转换成字符串,并将字符串用做缓存对象的属性名。如果在缓存中存在这个值,则直接返回它。否则,就调用既定的函数对实参进行计算,将计算结果缓存起来并返回。
- 直接使用
最大公约数
function gcd(a, b) {
var t; //这里省略对a和b的类型检查
if (a < b) t = b, b = a, a = t; //临时变量用来存储交换数值
while (b != 0) t = b, b = a % b, a = t; //确保a >=b
return a; //这是求最大公约数的欧几里德算法
}
var gcdmemo = memorize(gcd);
gedmemo(85, 187) // 17
第7版本,使用数组交换
function gcd(a, b) {
var t; //这里省略对a和b的类型检查
if (a < b)[a, b] = [b, a]; //临时变量用来存储交换数值
while (b != 0)[a, b] = [b, a % b] //确保a >=b
return a; //这是求最大公约数的欧几里德算法
}
var gcdmemo = memorize(gcd);
gedmemo(85, 187) // 17
- 对递归函数使用
var factorial = memoize(function (n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
})
factorial(5) // 120.对于4~1的值也有缓存