1. 函数式编程
当我们想要使用一个函数时,通常情况下其实就是想要将一些功能,逻辑等封装起来。相信大家对于封装这个概念并不陌生。
我们在初学时,往往会不由自主的使用命令式编程的风格来完成我们想要干的事情。因为命令式编程更加的简单,直白。例如我们现在有一个数组,array = [1, 3, 'h', 5, 'm', '4']
,现在想要找出这个数组中的所有类型为number
的子项。当我们使用命令式编程思维时,可能就会直接这样做:
var array = [1, 3, 'h', 5, 'm', '4'];
var res = [];
for(var i = 0; i < array.length; i ++) {
if (typeof array[i] === 'number') {
res.push(array[i]);
}
}
在这种实现方式中,我们平铺直叙的实现了我们的目的。这样做的问题在于,当我们在另外的时刻,想要找出另外一个数组中所有的子项时,我们不得不把同样的逻辑再写一次。当出现次数变多时,我们的代码也变得更加糟糕且难以维护。
而函数式编程的思维则建议我们将这种会多次出现的功能封装起来以备调用。以下为封装形式:
function getNumbers(array) {
var res = [];
array.forEach(function(item) {
if (typeof item === 'number') {
res.push(item);
}
})
return res;
}
// 以上是我们的封装,以下是功能实现
var array = [1, 3, 'h', 5, 'm', '4'];
var res = getNumbers(array);
因此当我们将功能封装之后,我们实现同样的功能时,只需要写一行代码。而如果未来需求变动,或者稍作修改,我们只需要对getNumbers
方法进行调整就可以了。而且我们在使用时,只需要关心这个方法能做什么,而不用关心他具体是怎么实现的。这也是函数式编程思维与命令式不同的地方之一。
函数式编程思维还具有以下几个特征:
- 函数是一等公民
所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。如:
var a = function foo() {} // 赋值
function fn(function() {}, num) {} // 函数作为参数
// 函数作为返回值
function var() {
return function() {
... ...
}
}
但是在实际应用中,我们大部分人并没有意识到,举个例子:
function delay() {
console.log('5000ms之后执行该方法.');
}
现在要做的是,如果要求你结合setTimeout
方法,让delay
方法延迟5000ms
执行,应该怎么做?大部分人最先想到:
var timer = setTimeout(function() {
delay();
}, 5000);
然而如果我们对函数是一等公民有一个深刻的认知,就会想到函数既然能够作为一个参数传入另外一个函数,那么我们是不是可以直接将delay
作为setTimeout
的第一个参数,而不用额外的多加一层匿名函数呢?
因此,其实最正确的解法应该这样写:
var timer = setTimeout(delay, 5000);
- 只用"表达式",不用"语句"
“表达式”(expression)是一个单纯的运算过程,总是有返回值;“语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
举个例子,假如我们的项目中,多处需要改变某个元素的背景色。因此我们可以这样封装一下:
var ele = document.querySelector('.test');
function setBackgroundColor(color) {
ele.style.backgroundColor = color;
}
// 多处使用
setBackgroundColor('red');
setBackgroundColor('#ccc');
进一步封装:
function setBackgroundColor(ele, color) {
ele.style.backgroundColor = color;
return color;
}
// 多处使用
var ele = document.querySelector('.test');
setBackgroundColor(ele, 'red');
setBackgroundColor(ele, '#ccc');
这样就可以更改任意元素的颜色了,同时也满足函数式编程,有输入,也有输出。
2. 纯函数
即一个函数的返回结果只依赖于它的参数,相同的输入总会得到相同的输出,并且不会产生副作用的函数,就是纯函数。
所谓"副作用",指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。即所谓的只要是同样的参数传入,返回的结果一定是相等的。
- 返回的结果只依赖它的参数
const a = 1;
const foo = (b) => a + b;
console.log(foo(2)); //3
函数foo
不是纯函数,因为它的输出还依赖于变量a
。
- 没有副作用
例如我们期望封装一个函数,能够得到传入数组的最后一项。那么可以通过下面两种方式来实现。
function getLast(arr) {
return arr[arr.length];
}
function getLast_(arr) {
return arr.pop();
}
var source = [1, 2, 3, 4];
var last = getLast(source); // 返回结果4 原数组不变
var last_ = getLast_(source); // 返回结果4 原数据最后一项被删除
getLast
与getLast_
虽然同样能够获得数组的最后一项值,但是getLast_
改变了原数组。而当原始数组被改变,那么当我们再次调用该方法时,得到的结果就会变得不一样。这就产生了我们所说的“副作用”,在我们看来是非常糟糕的。它会把我们的数据搞得非常混乱。在JavaScript
原生支持的数据方法中,也有许多不纯的方法,我们在使用时需要非常警惕,我们要清晰的知道原始数据的改变是否会留下隐患。
var source = [1, 2, 3, 4, 5];
source.slice(1, 3); // 纯函数 返回[2, 3] source不变
source.splice(1, 3); // 不纯的 返回[2, 3, 4] source被改变
source.pop(); // 不纯的
source.push(6); // 不纯的
source.shift(); // 不纯的
source.unshift(1); // 不纯的
source.reverse(); // 不纯的
// 我也不能短时间知道现在source被改变成了什么样子,干脆重新约定一下
source = [1, 2, 3, 4, 5];
source.concat([6, 7]); // 纯函数 返回[1, 2, 3, 4, 5, 6, 7] source不变
source.join('-'); // 纯函数 返回1-2-3-4-5 source不变
纯函数很严格,也就是说你几乎除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据。
3. 偏函数
偏函数用法是指创建一个调用另外一个部分–参数或者已经预置的函数–的函数的用法,他的定义很拗口,但是相信看完下面的这个例子就清楚多了。
在js中进行类型判断时,我们通常会通过以下代码来进行定义:
var toString = Object.prototype.toString();
var isString = function (obj) {
return toString.call(obj) === '[object String]';
};
var isFunction = function (obj) {
return toString.call(obj) === '[object Function]'
};
这段代码的问题很明显,就是我们需要重复的定义一些相似的函数,如果有更多的isXXX
,就会出现更多冗余的代码。为了解决重复的问题,我们引入一个新函数,这个新函数可以如工厂一样批量创建一些类似的函数,在下面的代码中,我们通过isType()
函数预先指定type
的值,然后返回一个新的函数:
var toString = Object.prototype.toString;
var isType = function (type) {
return function (obj) {
return toString.call(obj) === '[object ' + type + ']';
}
};
// 测试Array
var arr = [];
var isArray = isType('Array');
console.log(isArray(arr)); // true
// 测试Function
var func = function () {
// TODO Sth
};
var isFunction = isType('Function');
console.log(isFunction(func)); // true
可以看出,引入isType()
函数后,创建isString()
、isFunction()
函数就变得简单多了。这种通过指定部分参数来产生一个新的定制函数的形式就是偏函数。这时再看他的定义就清晰多了。
原文参考:
http://huziketang.com/books/react/lesson32
https://www.jianshu.com/p/69dede6f7e5f
《深入浅出node.js》