0%

函数式编程

纯函数:

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

1.函数柯里化和偏函数

柯里化

柯里化 (Currying) 的定义:

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

通俗的说法:

用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数

做一个闭包,返回一个函数,这个函数每次执行会改写闭包里面记录参数的数组。当这个函数判断参数个数够了,就去执行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function curry(fn, args) {
let length = fn.length;
args = args || [];
return function () {
let _args = [...args].concat([...arguments]);
if (_args.length < length) {
return curry.call(this, fn, _args);
} else {
return fn.apply(this, _args);
//之所以写成 this 是因为希望根据环境的不同而设置不同的 this 值
}
}
}

let fn = curry(function(a, b, c) {
console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

ES6写法

1
let curry = (fn, arr = []) => fn.length === arr.length ? fn(...arr) : (...args) => curry(fn, [...arr, ...args]);

以上的更像是柯里化与偏函数的结合体

偏函数

偏函数 (Partial application) 的定义:

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.

在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。

什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。

柯里化和偏函数的区别

柯里化是将一个多参数函数转化成多个单参数函数,也就是将一个n元函数转换成n个一元函数

偏函数是将一个n元的函数转换成一个n-x元函数

1
2
3
4
5
6
7
8
//第一版
function partial(fn) {
var args = [].slice.call(arguments, 1);
return function() {
var newArgs = args.concat([].slice.call(arguments));
return fn.apply(this, newArgs);
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 第二版
var _ = {};

function partial(fn) {
var args = [].slice.call(arguments, 1);
return function() {
var position = 0, len = args.length;
var _args = []
for(var i = 0; i < len; i++) {
_args.push(args[i] === _ ? arguments[position++] : args[i])
}
while(position < arguments.length) _args.push(arguments[position++]);
return fn.apply(this, _args);
};
};
var subtract = function(a, b) { return b - a; };
subFrom20 = partial(subtract, _, 20);
console.log(subFrom20(5));
console.log(subFrom20(4));
//占位符功能

Thunk

Thunk方法主要使用在有cb(回调函数)参数的方法中,是指将参数拆分为cb部分和非cb部分

2.惰性函数

  • 惰性函数的实现原理就是重新定义函数。
  • 惰性思想的精髓:能一次搞定的事,我绝不做第二次

需求

我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。

解决一:普通方法

1
2
3
4
5
6
var t;
function foo() {
if (t) return t;
t = new Date()
return t;
}

问题有两个,一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断。

解决二:闭包

我们很容易想到用闭包避免污染全局变量。

1
2
3
4
5
6
7
8
var foo = (function() {
var t;
return function() {
if (t) return t;
t = new Date();
return t;
}
})();

然而还是没有解决调用时都必须进行一次判断的问题。

解决三:函数对象

函数也是一种对象,利用这个特性,我们也可以解决这个问题。

1
2
3
4
5
function foo() {
if (foo.t) return foo.t;
foo.t = new Date();
return foo.t;
}

依旧没有解决调用时都必须进行一次判断的问题。

解决四:惰性函数

不错,惰性函数就是解决每次都要进行判断的这个问题,解决原理很简单,重写函数。

1
2
3
4
5
6
7
var foo = function() {
var t = new Date();
foo = function() {
return t;
};
return foo();
};

更多应用

DOM 事件添加中,为了兼容现代浏览器和 IE 浏览器,我们需要对浏览器环境进行一次判断:

1
2
3
4
5
6
7
8
9
// 简化写法
function addEvent (type, el, fn) {
if (window.addEventListener) {
el.addEventListener(type, fn, false);
}
else if(window.attachEvent){
el.attachEvent('on' + type, fn);
}
}

问题在于我们每当使用一次 addEvent 时都会进行一次判断。

利用惰性函数,我们可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
function addEvent (type, el, fn) {
if (window.addEventListener) {
addEvent = function (type, el, fn) {
el.addEventListener(type, fn, false);
}
}
else if(window.attachEvent){
addEvent = function (type, el, fn) {
el.attachEvent('on' + type, fn);
}
}
addEvent(type, el, fn)
}

当然我们也可以使用闭包的形式:

1
2
3
4
5
6
7
8
9
10
11
12
var addEvent = (function(){
if (window.addEventListener) {
return function (type, el, fn) {
el.addEventListener(type, fn, false);
}
}
else if(window.attachEvent){
return function (type, el, fn) {
el.attachEvent('on' + type, fn);
}
}
})();

当我们每次都需要进行条件判断,其实只需要判断一次,接下来的使用方式都不会发生改变的时候,想想是否可以考虑使用惰性函数。

3.函数组合

需求

我们需要写一个函数,输入 ‘kevin’,返回 ‘HELLO, KEVIN’。

试想我们写个 compose 函数:

1
2
3
4
5
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};

greet 函数就可以被优化为:

1
2
var greet = compose(hello, toUpperCase);
greet('kevin');

利用 compose 将两个函数组合成一个函数,让代码从右向左运行,而不是由内而外运行,可读性大大提升。这便是函数组合。

compose

underscore 的 compose 函数的实现:

1
2
3
4
5
6
7
8
9
10
function compose() {
let args = arguments;
let start = args.length - 1;
return function() {
let i = start;
let result = args[start].apply(this, arguments);
while (i--) result = args[i].call(this, result);
return result;
};
};

ES6

1
2
const compose = (...fns) =>  
(arg) => fns.reduce( (composed, f) => f(compose), arg)

现在的 compose 函数已经可以支持多个函数了,然而有了这个又有什么用呢?

在此之前,我们先了解一个概念叫做 pointfree。

pointfree

pointfree 指的是函数无须提及将要操作的数据是什么样的。依然是以最初的需求为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 需求:输入 'kevin',返回 'HELLO, KEVIN'。

// 非 pointfree,因为提到了数据:name
var greet = function(name) {
return ('hello ' + name).toUpperCase();
}

// pointfree
// 先定义基本运算,这些可以封装起来复用
var toUpperCase = function(x) { return x.toUpperCase(); };
var hello = function(x) { return 'HELLO, ' + x; };

var greet = compose(hello, toUpperCase);
greet('kevin');

我们再举个稍微复杂一点的例子,为了方便书写,我们需要借助在 curry 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 需求:输入 'kevin daisy kelly',返回 'K.D.K'

// 非 pointfree,因为提到了数据:name
var initials = function (name) {
return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};

// pointfree
// 先定义基本运算
var split = curry(function(separator, str) { return str.split(separator) })
var head = function(str) { return str.slice(0, 1) }
var toUpperCase = function(str) { return str.toUpperCase() }
var join = curry(function(separator, arr) { return arr.join(separator) })
var map = curry(function(fn, arr) { return arr.map(fn) })

var initials = compose(join('.'), map(compose(toUpperCase, head)), split(' '));

initials("kevin daisy kelly");

从这个例子中我们可以看到,利用柯里化(curry)和函数组合 (compose) 非常有助于实现 pointfree。

利用 curry,我们能够做到让每个函数都先接收数据,然后操作数据,最后再把数据传递到下一个函数那里去。

Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。即不使用所要处理的值,只合成运算过程。

pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试也变得轻而易举。

4.函数记忆

定义

函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。

原理

实现这样一个 memoize 函数很简单,原理上只用把参数和对应的结果数据存到一个对象中,调用时,判断参数对应的数据是否存在,存在就返回对应的结果数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 第一版 (来自《JavaScript权威指南》)
function memoize(f) {
var cache = {};
return function(){
var key = arguments.length + Array.prototype.join.call(arguments, ",");
if (key in cache) {
return cache[key]
}
else {
return cache[key] = f.apply(this, arguments)
}
}
}

适用场景

我们以斐波那契数列为例:

1
2
3
4
5
6
7
8
9
10
var count = 0;
var fibonacci = function(n){
count++;
return n < 2? n : fibonacci(n-1) + fibonacci(n-2);
};
for (var i = 0; i <= 10; i++){
fibonacci(i)
}

console.log(count) // 453

我们会发现最后的 count 数为 453,也就是说 fibonacci 函数被调用了 453 次!也许你会想,我只是循环到了 10,为什么就被调用了这么多次,所以我们来具体分析下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
当执行 fib(0) 时,调用 1

当执行 fib(1) 时,调用 1

当执行 fib(2) 时,相当于 fib(1) + fib(0) 加上 fib(2) 本身这一次,共 1 + 1 + 1 = 3

当执行 fib(3) 时,相当于 fib(2) + fib(1) 加上 fib(3) 本身这一次,共 3 + 1 + 1 = 5

当执行 fib(4) 时,相当于 fib(3) + fib(2) 加上 fib(4) 本身这一次,共 5 + 3 + 1 = 9

当执行 fib(5) 时,相当于 fib(4) + fib(3) 加上 fib(5) 本身这一次,共 9 + 5 + 1 = 15

当执行 fib(6) 时,相当于 fib(5) + fib(4) 加上 fib(6) 本身这一次,共 15 + 9 + 1 = 25

当执行 fib(7) 时,相当于 fib(6) + fib(5) 加上 fib(7) 本身这一次,共 25 + 15 + 1 = 41

当执行 fib(8) 时,相当于 fib(7) + fib(6) 加上 fib(8) 本身这一次,共 41 + 25 + 1 = 67

当执行 fib(9) 时,相当于 fib(8) + fib(7) 加上 fib(9) 本身这一次,共 67 + 41 + 1 = 109

当执行 fib(10) 时,相当于 fib(9) + fib(8) 加上 fib(10) 本身这一次,共 109 + 67 + 1 = 177

所以执行的总次数为:177 + 109 + 67 + 41 + 25 + 15 + 9 + 5 + 3 + 1 + 1 = 453 次!

如果我们使用函数记忆呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
var count = 0;
var fibonacci = function(n) {
count++;
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};

fibonacci = memoize(fibonacci)

for (var i = 0; i <= 10; i++) {
fibonacci(i)
}

console.log(count) // 11

我们会发现最后的总次数为 11 次,因为使用了函数记忆,调用次数从 453 次降低为了 12 次!

-------------本文结束感谢您的阅读-------------