相同的输入总会得到相同的输出,并且不会产生副作用的函数,就是纯函数。
我们可以通过一个是否会改变原始数据的两个同样功能的方法来区别纯函数与不纯函数之间的不同。
我们期望封装一个函数,能够获取到传入数组的最后一项。那么我们可以通过如下两种方式来实现。
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不变
与这种会改变原始数据的函数相比,纯函数明显更加可靠。很显然我们都不希望自己的数据经过几次调用之后就变得一团糟。
纯函数还有一个重要的特点,那就是除了传入的参数之外,不依赖任何外界的信息与状态。例如下面这个不纯的例子。
var name = 'Jake';
function sayHello() {
return 'Hello, ' + name;
}
sayHello(); // Hello, Jake
// 当我们有其他需求需要改变name的值
name = 'Tom';
sayHello(); // Hello, Tom
同样的调用,但是由于sayHello函数依赖于外界的name变量,因此当外界发生变化时,函数的运行结果就变得不一样。很显然这并不是我们封装函数时希望看到的状况。因为这样的变化无法预测。因此,如果优化上面的例子,那么我们应该把name当做一个参数传入,这样我们就能够直观的知道,该函数执行时会输出上面结果。
function sayHello(name) {
return 'Hello, ' + name;
}
纯函数的可移植性
无论我们在封装一个函数,一个库,一个组件时,其实我们都期望一次封装,多处使用。而纯函数则刚好具备这样的特性。
纯函数不依赖参数之外的值,因此纯函数的依赖非常明确。也正是如此,我们能够把一些常用的功能封装成为一个公共方法,以备以后遇到类似的场景会用到,我们就不在重新封装了。
我们想大家应该知道一个页面的url里常常会在"?"后面带有参数,例如https://www.baidu.com/s?tn=baidu&wd=javascript&rsv_sug=1
。很多时候我们需要从这段url中,获取到某些参数对应的值。例如这个例子中的"wd"的值为javascript。那么想要封装这样一个纯函数我们应该怎么做呢?如下:
function getParams(url, param) {
if (!/\?/.test(url)) {
return null;
}
var search = url.split('?')[1];
var array = search.split('&');
for(var i = 0; i < array.length; i++) {
var tmp = array[i].split('=');
if (tmp[0] === param) {
return decodeURIComponent(tmp[1]);
}
}
return null;
}
var url = 'https://www.baidu.com/s?tn=baidu&wd=javascript&rsv_sug=1';
getParams(url, 'wd'); // javascript
虽然getParams并非完全健壮,但是已经足以体现纯函数可移植的特点。我们可以在任何需要从url中取得参数对应值的地方调用该方法。
纯函数的可缓存性
在实践中我们可能会处理大量的数据,例如根据日期,得到当日相关的数据,并处理成为我们前端能够使用的数据。假设我们封装了一个process方法来处理每天的数据,而这个处理过程会很复杂。如果我们不缓存处理结果,那么每次想要得到当天的数据时,就不得不从原始数据再转换一次。当数据的处理足够复杂,那么我想着并不是性能最优的解决方案。而纯函数的特点是,相同的输入总能得到相同的输出,因此如果我们将处理过的每一天的数据都缓存起来,那么当我们第二次或者更多次数的想要得到当天数据时,就不用经历复杂的处理过程了。
// 传入日期,获取当天的数据
function process(date) {
var result = '';
// 假设这中间经历了复杂的处理过程
return result;
}
function withProcess(base) {
var cache = {}
return function() {
var date = arguments[0];
if (cache[date]) {
return cache[date];
}
return base.apply(base, arguments);
}
}
var _process = withProcess(process);
// 经过上面一句代码处理之后,我们就可以使用_process来获取我们想要的数据,如果数据存在,会返回缓存中的数据,如果不存在,则会调用process方法重新获取。
_process('2017-06-03');
_process('2017-06-04');
_process('2017-06-05');
上面例子中利用了闭包的特性,将处理过的数据都缓存在cache中。这种方式算是高阶函数的一种运用。我们将在下面一个小节介绍高阶函数。也真是因为纯函数的可靠性,才能够让我们非常放心的确保缓存的数据一定就是我们想要的正确结果。
我想到这里,大家已经明白了什么是纯函数,纯函数有什么特点,以及我们为什么要尽量使用纯函数。虽然在实践中并不是所有的场景都能够使用纯函数,我们只需要尽量在合适的场景使用即可。