函数式编程简介
函数式编程是一种范式,我们能够以此创建仅依赖输入就可以完成自身逻辑的函数。这保证了当函数多次调用时仍然返回相同的结果。函数不会改变任何外部环境的变量,这将产生可缓存,可测试的代码库。
引用透明性:函数对于相同的输入都将返回相同的值。
纯函数:相同的输入返回相同输出的函数,该函数不应依赖任何外部变量,也不应改变任何外部变量。
高阶函数:接收函数作为参数或者返回函数作为输出的函数。
高阶函数举例(为了讲清楚内容,这里的函数都是低效的):
every(数组通过计算,若所有元素是否为true,则为true)
const every = (arr,fn) => {
let result = true;
for(const value of arr)
result = result && fn(value)
return result
}
every([NaN, NaN, NaN], isNaN) // true
every([NaN, NaN, 1], isNaN) // false
some(数组通过计算,只要有一个为true,那么结果为true)
const some = (arr,fn) => {
let result = false;
for(const value of arr)
result = result || fn(value)
return result
}
some([NaN,NaN, 4], isNaN) // true
some([3,4, 4], isNaN) // false
unless(如果传入值是false时,则执行函数)
const unless = (predicate,fn) => {
if(!predicate)
fn()
}
times(从0开始迭代多少次)
const times = (times, fn) => {
for (var i = 0; i < times; i++) fn(i);
}
// 打印0~99之间(共100个)的偶数
times(100, function(n) {
unless(n % 2, function() {
console.log(n, "is even");
});
});
闭包就是一个内部函数。如下,其中函数inner被称为闭包函数。
function outer() {
function inner() {
}
}
闭包可访问的作用域:
如下:
let a = "全局作用域";
function outer() {
let b = "闭包所在的外部函数的作用域";
function inner() {
let c = "自身函数内的作用域";
console.log(a, b, c);
}
}
闭包可以记住上下文:
var fn = (arg) => {
let outer = "visible";
let innerFn = () => {
console.log(outer);
console.log(arg);
}
}
var closureFn = fn(5);
closureFn();// 打印 "visible" 和 5
上述代码需要注意closureFn函数已经在fn外部了,但是仍可以访问fn的变量outer,这个是闭包最重要的特征。
高阶函数举例(续):
tap(接收一个value返回一个函数,当函数执行时第一个参数是value)
const tap = (value) =>
(fn) => (
typeof(fn) === 'function' && fn(value),
console.log(value)
)
tap("fun")(value => console.log("value is " + value));// 打印 "value is fun"
unary (将多参函数转化为一个参数的函数)
const unary = (fn) =>
fn.length === 1
? fn
: (arg) => fn(arg)
['1', '2', '3'].map(parseInt);// 经典面试题 因为parseInt接受的第二个参数表示多少进制 导致最后返回的是 [1, NaN, NaN,]
['1', '2', '3'].map(unary(parseInt));// 返回 [1, 2, 3]
once (函数只运行一次)
const once = (fn) => {
let done = false;
return function () {
return done ? undefined : ((done = true), fn.apply(this, arguments))
}
}
var doPayment = once(() => {
console.log("Payment is done")
})
// 原函数执行,打印 "Payment is done"
doPayment()
// 原函数不执行
doPayment()
memoized (函数记忆化)
const memoized = (fn) => {
const lookupTable = {};
return (arg) => lookupTable[arg] || (lookupTable[arg] = fn(arg));
}
// 计算阶乘
var factorial = (n) => {
if (n === 0) {
return 1;
}
// This is it! Recursion!!
return n * factorial(n - 1);
}
// 记忆化阶乘函数
let fastFactorial = memoized(factorial);
// 下面打印 "Fast Factorial of 3 is 6"
console.log("Fast Factorial of 2 is", fastFactorial(3));
// 下面打印 "Fast Factorial of 3 is 6" 第二次的时候记忆化后的参数不再重新计算 直接返回6
console.log("Fast Factorial of 3 is", fastFactorial(3));
map(将数组转化为一个新的数组)
const map = (array,fn) => {
let results = []
for(const value of array)
results.push(fn(value))
return results;
}
let squaredArray = map([1,2,3], (x) => x * x);
console.log(squaredArray); // [1, 4, 9]
filter(过滤函数)
const filter = (array,fn) => {
let results = []
for(const value of array)
(fn(value)) ? results.push(value) : undefined
return results;
}
// 实例:返回数组中的基数
filter([1, 2, 3, 4], (x)=>x % 2 === 1);// [1, 3]
concatAll(数组扁平化,实际上就是我们常用的flatten,作用是将多个数组,合并成一个数组)
const concatAll = (array) => {
let results = []
for(const value of array)
results.push.apply(results, value);
return results;
}
concatAll([[1, 2, 3], [3, 4, 5]]); // 结果为 [1, 2, 3, 3, 4, 5]
reduce(累计计算)
const reduce = (array, fn, initialValue) => {
let accumlator;
if(initialValue != undefined)
accumlator = initialValue;
else
accumlator = array[0];
if(initialValue === undefined)
for(let i=1;i<array.length;i++)
accumlator = fn(accumlator,array[i])
else
for(const value of array)
accumlator = fn(accumlator,value)
return accumlator
}
reduce([1,2,3,4,5],(acc,val) => acc + val,0); // 计算加法 返回 15
reduce([1,2,3,4,5],(acc,val) => acc * val,1); // 计算乘法 返回 120
zip(合并两个指定的函数)
const zip = (leftArr,rightArr,fn) => {
let index, results = [];
for(index = 0;index < Math.min(leftArr.length, rightArr.length);index++)
results.push(fn(leftArr[index],rightArr[index]));
return results;
}
zip([1, 2, 3],[4, 5, 6], (x, y) => x + y)); // 返回 [5, 7, 9]
一元函数:只接受一个参数的函数。
二元函数:只接受两个参数的函数。
变参函数:接受可变数量参数的函数。
如下:
const identify = (x) => x; // 一元函数
const add = (x, y) => x + y; // 二元函数
function variadic(a) { // 变参函数
console.log(a);
console.log(arguments);
}
variadic(1, 2, 3); // 打印1 [1, 2, 3] 需要注意的是arguments不是数组类型 是Arguments类的实例
function variadic2(a, ...variadic) { // 变参函数
console.log(a);
console.log(variadic);
}
variadic2(1, 2, 3); // 打印1 [2, 3]
柯里化:柯里化是把一个多参数函数转化为可嵌套的一元函数的过程。
一元柯里化:
const curry = (binaryFn) => {
return function (firstArg) {
return function (secondArg) {
return binaryFn(firstArg, secondArg);
};
};
};
const add = (x,y) => x + y;
let autoCurriedAdd = curry(add)
console.log("Curried summation", autoCurriedAdd(2)(2)); // 打印 "Curried summation 4"
多元柯里化:
const curryN =(fn) => {
if(typeof fn!=='function'){
throw Error('No function provided');
}
return function curriedFn(...args){
//make it bold
if(args.length < fn.length){
return function(){
return curriedFn.apply(null, args.concat( [].slice.call(arguments) ));
};
}
//make it bold
return fn.apply(null, args);
};
};
// 实战:使用柯里化实现一秒后延迟调用函数
const setTimeoutWrapper = (time,fn) => {
setTimeout(fn,time);
}
// 使用柯里化
const delayTenMs = curryN(setTimeoutWrapper)(1000);
delayTenMs(() => console.log("Do X task")); // 一秒后打印 "Do X task"
delayTenMs(() => console.log("Do Y task")); // 一秒后打印 "Do Y task"
回顾上面柯里化的例子,由于柯里化的参数是从左往右的,所以我们不得不定义一个转化函数setTimeoutWrapper将函数转化为多个嵌套函数,也就是curryN调用完curryN(setTimeoutWrapper)再调用一下返回的函数,并传递参数1000。那么能否提供一个函数,使得函数某几个参数始终都是相同的呢,这里就用到了偏函数,如下:
const partial = function (fn,...partialArgs){
let args = partialArgs.slice(0);
return function(...fullArguments) {
let arg = 0;
for (let i = 0; i < args.length && arg < fullArguments.length; i++) {
if (args[i] === undefined) {
args[i] = fullArguments[arg++];
}
}
return fn.apply(this, args);
};
};
// 使用偏函数 函数1秒后执行
// 本书中的例子使用undefined来表示后续需要传入的参数
// 这里setTimeout第一个参数由调用时候决定 第二个参数固定永远是1000 表示1秒后调用
let delayTenMsPartial = partial(setTimeout, undefined, 1000);
delayTenMsPartial(() => console.log("Do X. . . task"))
delayTenMsPartial(() => console.log("Do Y . . . . task"))
Unix中使用管道符号“|”来组合一些命令,使得前一个命令的输出是后一个命令的输入。如我们要统计某个文本文件中“World”出现的次数,可以使用下面的命令。
cat test.txt | grep "World" | wc
函数的组合:将一个函数的输出当成另一个函数的输入,最终把两者合并成一个函数。
var compose = (a, b) => (c) => a(b(c))
let number = compose(Math.round,parseFloat); // 将一个函数转化为浮点型数字后并四舍五入
console.log("Number is ",number("3.56")); // 打印 "Number is 4"
组合多个函数:
const composeN = (...fns) =>
(value) =>
reduce(fns.reverse(),(acc, fn) => fn(acc), value);
// 应用:判断一句话有奇数个单词还是偶数个
let splitIntoSpaces = (str) => str.split(" ");
let count = (array) => array.length;
let oddOrEven = (ip) => ip % 2 == 0 ? "even" : "odd";
// 使用组合函数 先拆分字符串 然后计算个数 随后看个数是奇数个还是偶数个
let oddOrEvenWords = composeN(oddOrEven,count,splitIntoSpaces);
// 打印 "Even or odd via compose ? odd"
console.log("Even or odd via compose ?",oddOrEvenWords("hello your reading about composition"));
上述组合函数参数是从右往左依次调用的,如果是从左往右那么就叫做管道了,也有成为序列。管道是组合的复制品,唯一修改的地方就是数据流的方向。
const pipe = (...fns) =>
(value) =>
reduce(fns,(acc, fn) => fn(acc), value);
// 在上例中使用管道
let oddOrEvenWords = pipe(splitIntoSpaces,count,oddOrEven);
组合满足结合律:compose(f, compose(g, h)) === compose(compose(f, g), h)
函子:函子是一个普通对象(在其他语言中可能是一个类),它实现了map函数,在遍历每个对象值的时候生成一个新的对象。
实际上数组就是函子!下面一步一步实现一个普通的函子:
// 首先定义一个容器 由于需要new一个对象 所以这里没使用箭头函数
// 函子只跟提供map函数有关 跟类名是无关的 这里的Container也可以换成其他名称
const Container = function(val) {
this.value = val;
}
// 这里为了方便创建对象 添加了一个of方法
Container.of = function(value) {
return new Container(value);
}
// 只要提供了map方法 使用Container创建的对象就是函子
// map方法实现需要根据实际情况来确定 这里提供了一种实现
Container.prototype.map = function(fn){
return Container.of(fn(this.value));
}
// 使用of方法来创建函子
let testValue = Container.of(3);// 函子的值是一个数字
let testObj = Container.of({a:1});// 函子的值是一个对象
let testArray = Container.of([1,2]);// 函子的值是一个数组
let testContainer = Container.of(Container.of(3));// 函子的值也可以是一个函子
// 实例:将3加倍2次
let double = (x) => x + x;
console.log(Container.of(3).map(double).map(double).value); // 打印12
现在简绍一种新的函子,叫MayBe。MayBe函子是用来处理函数式编程空值问题的,实现如下:
// 定义一个容器 跟上面一样的 就是改了一个名字
const MayBe = function(val) {
this.value = val;
}
// of方法用来方便创建对象的 没必要每次new来new去
MayBe.of = function(val) {
return new MayBe(val);
}
// 这里为了方便添加了一个辅助方法 用来判空的
MayBe.prototype.isNothing = function() {
return (this.value === null || this.value === undefined);
};
// 最重要的方法map 如果是空的那么返回一个空的函子 否则返回函数执行结果的函子
MayBe.prototype.map = function(fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value));
};
// MayBe函子的使用
MayBe.of("string").map((x) => x.toUpperCase()).value // 返回 "STRING"
MayBe.of(null).map((x) => x.toUpperCase()).value // 返回 null 注意这里索然返回了null 但是程序还可以运行
// map 可以连用
MayBe.of("George").map((x) => x.toUpperCase()).map((x) => "Mr. " + x).value //返回 "Mr. GEORGE"
MayBe.of("George").map(() => undefined).map((x) => "Mr. " + x).value // 返回 null 运行过程中某一步返回空也不会导致程序奔溃
MayBe函子中每一个map函数都会执行,但是如果某一个map返回的是空,那么它后面的map函数的参数函数就都不会执行了,单map函数仍然会执行。
MayBe函子解决了空值的问题,Either函子解决或运算,Either函子实现如下:
const Nothing = function(val) {
this.value = val;
};
Nothing.of = function(val) {
return new Nothing(val);
};
Nothing.prototype.map = function(f) {
return this;
};
const Some = function(val) {
this.value = val;
};
Some.of = function(val) {
return new Some(val);
};
Some.prototype.map = function(fn) {
return Some.of(fn(this.value));
}
// Either.Some 与 Either.Nothing 这两个函子统称为Either函子
const Either = {
Some : Some,
Nothing: Nothing
}
// 到这里 我想你应该还是一头雾水 这个有什么用处呢?
// 由上 实际上Either.Nothing无论调用多少个map始终返回的是自己
// 应用:如果一个数是基数那么乘以5然后加100 如果这个数是偶数则返回自己
let oddOrEven = (num) => num % 2 === 1 ? Either.Some.of(num) : Either.Nothing.of(num);
let mul5 = num => num * 5;
let add100 = num => num + 100;
oddOrEven(5).map(mul5).map(add100).value // 返回 125
oddOrEven(6).map(mul5).map(add100).value // 返回 6
Either函子在实际应用时,如果值在计算中不再参与计算的时候就使用Either.Nothing否则使用Either.Some。
Point函子:Point函子是函子的子集,它具有of方法。
我们写的MayBe函子和Either都实现了of方法,所以这两个都是Point函子。另外我们常用的数组,ES6也新增了of方法,所以它也是Point函子。
Monad也是一种函子,估计你看到Monad这个词你就头大了。此时你的内心:“卧槽!又要学习一个新的函子,真心学不动了,求别更新了!!!”
其实,函子这块就是纸老虎,各种名字天花乱坠,实际上都是很简单的,Monad也不例外,先看看Monad的定义。
Monad就是一个含有chain方法的函子。
是不是纸老虎,在说chain方法之前我们先简单的说一下另一个方法join,上面我们创建MayBe函子以后最后都要调用.value来返回真正的值,这里添加一个join方法,如果不为空的时候就返回函子的value否则返回MayBe.of(null),如下:
MayBe.prototype.join = function() {
return this.isNothing() ? MayBe.of(null) : this.value;
}
// 使用
MayBe.of(5).join() // 返回5
MayBe.of(MayBe.of(3)).join().join() // 返回3 有多少个of最后就得调用多少个join()
MayBe.of(MayBe.of(3).join()).join() // 返回3 这样也是可以的
我们一般使用MayBe的时候都会调用map函数的,大多数情况最后一个map调用完我们还会调用上面的join方法来获取value。为了简化最后这两步我们引入了chain方法:
MayBe.prototype.chain = function(f){
return this.map(f).join()
}
// 有了chain方法 就可以简化一下之前的代码
MayBe.of("string").chain((x) => x.toUpperCase()) // 返回 "STRING"
MayBe.of(null).chain((x) => x.toUpperCase()) // 返回 MayBe {value: null}
MayBe.of("George").map((x) => x.toUpperCase()).chain((x) => "Mr. " + x) //返回 "Mr. GEORGE"
这就是Monad的全部内容,没错就这!相信你已经理解的很深入了!
我们回顾一下这两节的内容:有map方法的对象就是函子,有of方法的函子就是Point函子,有chain方法的函子就是Monad函子。
本书最后一章介绍了ES6的Generator的使用,这里就简述一下:
// 创建Generator(就是函数名和function之间加一个*)
function* gen() {
return 'first generator';
}
// 调用Generator 此时只返回一个Generator对象 函数并不会立即执行
let generatorResult = gen();
// 执行函数
generatorResult.next(); // 返回 {value: "first generator", done: true}
// 如果要获取Generator的值则需要使用 generatorResult.next().value
// 执行完可以继续调用
generatorResult.next(); // 返回 {value: undefined, done: true}
// 调用Generator可以使用yield关键字
function* generatorSequence() {
yield 'first';
yield 'second';
yield 'third';
}
let generatorSequenceResult = generatorSequence();
generatorSequenceResult.next(); // 返回 {value: "first", done: false}
generatorSequenceResult.next(); // 返回 {value: "second", done: false}
generatorSequenceResult.next(); // 返回 {value: "third", done: false}
generatorSequenceResult.next(); // 返回 {value: undefined, done: true}
// 需要注意的是generatorSequence有三个yield,虽然第三个yield是最后一行代码
// 但是当他执行完后下一次还会执行一下return的代码 默认相当于return undefined
// 向Genetator传参
function* sayFullName() {
var firstName = yield 123;
var secondName = yield 456;
console.log(firstName + secondName);
}
let fullName = sayFullName();
fullName.next(); // 返回 {value: 123, done: false}
// 注意第一个nuxt调用的时候会执行代码到第一个yield处
// 当第二次执行next的时候才会给第一个yield传参
fullName.next('anto'); // 返回 {value: 456, done: false}
fullName.next('aravinth'); // 打印 "antoaravinth" 返回 {value: undefined, done: true}