04-函数即……函数
13.3 函数即……函数
至此我们已经讨论了通过一些更明显的公式来思考函数,是时候以函数来思考函数了。如果你是一个数学家,可能会把函数想象成关系,先有输入,而后有输出。每一个输入都有其对应的输出。那些遵守数学定义的函数会被开发人员称为纯函数。甚至在有些编程语言(例如Haskell)中,只允许纯函数存在。
纯函数与前面提到的函数有什么不同呢?首先,纯函数必须做到,对同一组输入,始终返回相同的结果。这么看来, isCurrentYearLeapYear 就不是一个纯函数,因为它的返回值取决于被调用的时间(某一年可能返回true,而下一年可能返回false)。其次,该函数不能有副作用。也就是说,调用该函数不能改变程序的状态。在关于函数的讨论中,还没有碰到产生副作用的函数(一般不把控制台输出当成副作用)。看一个简单的例子:
const colors = ['red', 'orange', 'yellow', 'green',
'blue', 'indigo', 'violet'];
let colorIndex = -1;
function getNextRainbowColor() {
if(++colorIndex >= colors.length) colorIndex = 0;
return colors[colorIndex];
}
getNextRainbowColor 函数每次被调用时,都会循环返回彩虹中7种颜色中的一种。这个函数不符合纯函数的两大规则:相同的输入(没有参数,所以没有输入)每次返回的结果不同;而且会产生副作用(改变了 color 变量的索引)。很明显, colorIndex 变量不是函数的一部分,而调用 getNextRainbowColor 后它的值却变了,这就是一个副作用。
回到闰年问题上来,怎么把它变成一个纯函数呢?非常简单:
function isLeapYear(year) {
if(year % 4 !== 0) return false;
else if(year % 100 != 0) return true;
else if(year % 400 != 0) return false;
else return true;
}
修改后,针对相同的输入,函数的返回值始终一样,并且没有副作用,这样它就变成了一个纯函数。
函数 getNextRainbowColor 更有意思。可以通过将外部变量放入闭包从而消除副作用:
const getNextRainbowColor = (function() {
const colors = ['red', 'orange', 'yellow', 'green',
'blue', 'indigo', 'violet'];
let colorIndex = -1;
return function() {
if(++colorIndex >= colors.length) colorIndex = 0;
return colors[colorIndex];
};
})();
这样,就得到了一个没有副作用的函数,但这仍不是一个纯函数,因为对于相同的输入,它的返回值可能不一样。为了解决这个问题,一定要小心地使用该函数。因为可能会反复调用它,例如,每半秒钟就改变一个浏览器中元素的颜色(在第18章会学习更多关于浏览器的代码)。
setInterval(function() {
document.querySelector('.rainbow')
.style['background-color'] = getNextRainbowColor();
}, 500);
这段代码看起来也许没那么糟糕,意图也很明显:一些class为rainbow的HTML元素会循环显示彩虹的颜色。然而问题在于,如果其他地方也调用了 getNextRainbowColor() ,就会和这段代码产生冲突!所以应该停下想想,使用有副作用的函数是否合适。在这个例子中,使用迭代器可能会更好一些:
function getRainbowIterator() {
const colors = ['red', 'orange', 'yellow', 'green',
'blue', 'indigo', 'violet'];
let colorIndex = -1;
return {
next() {
if(++colorIndex >= colors.length) colorIndex = 0;
return { value: colors[colorIndex], done: false };
}
};
}
这样一来, getRainbowIterator 就是一个纯函数:每次的返回值都相同(也就是一个迭代器),并且没有副作用。它的用法变了,但也更安全了:
const rainbowIterator = getRainbowIterator();
setInterval(function() {
document.querySelector('.rainbow')
.style['background-color'] = rainbowIterator.next().value;
}, 500);
大家可能会觉得这种做法有掩耳盗铃的嫌疑: next() 方法每次的返回值都不一样啊!确实不一样,但要注意, next() 是一个方法,不是函数。它运行在自己所属对象的上下文中,所以它的行为受控于该对象。如果在程序的其他地方调用了 getRainbowIterator ,就会生成不同的迭代器,并且各自独立,互不影响。