《JavaScript函数式编程思想》——副作用和不变性

版权声明: https://blog.csdn.net/starrow/article/details/87077578

第6章  副作用和不变性


6.1  副作用
6.2  纯函数
6.2.1  外部变量
6.2.2  实现
6.2.3  函数内部的副作用
6.2.4  闭包
6.3  不变性
6.3.1  哲学上的不变性与身份
6.3.2  简单类型和复合类型
6.3.3  值类型和引用类型
6.3.4  可变类型和不可变类型

6.3.5  可变数据类型的坏处

在详细讨论完不变性的含义和其与其他概念的关系之后,终于可以来看看可变数据的坏处了。我们知道函数的副作用是有害的,编写函数时最容易犯的导致副作用的错误就是修改了数组和对象这样的可变类型的参数。

//求一个数字数组的算术平均数。
function mean(nums) {
    return f.div(f.sum(nums), nums.length);
}

//求一个数字数组的平方平均数,也就是各个元素的平方的算术平均数的平方根。
function rms(nums) {
    //为了利用求算术平均数的函数,先将数组的每个元素换成其平方值。
    for (let i = 0; i < nums.length; i++) {
        let n = nums[i];
        nums[i] = f.mul(n, n);
    }
    let m = mean(nums);
    return Math.sqrt(m);
}

let nums = [1, 2, 3, 4, 5];
f.log(mean(nums));
//=> 3

f.log(rms(nums));
//=> 3.3166247903554

上面代码中求算术平均数和平方平均数的函数看似都工作正常,但此时如果再求原数组的平均数,就会发现结果不同了。

f.log(mean(nums));
//=> 11

原因是函数rms不当地修改了数组参数,纠正的方法有很多,可以创建一个新的数组来容纳平方值,或者直接调用没有副作用的map函数。再来看函数返回值是可变数据类型的例子。我们想在母亲节送上祝福,为此写了一个函数计算出今年母亲节的日期。

function getMotherDay() {
    let now = new Date(Date.now());
    //今年4月的最后一天。注意月份数字从0开始,日期数字从1开始。
    let date = new Date(now.getFullYear(), 4, 0);
    //4月份的最后一天是星期几。
    let day = date.getDay();
    //从4月份的最后一天开始计算,出现的第2个周日就是母亲节。
    //注意表示周几的数字是从0开始的。
    let secondSunday = 14 - day;
    date.setDate(secondSunday);
    return date;
}

f.log(getMotherDay());
//=> Fri Apr 13 2018 00:00:00 GMT+0800 (China Standard Time)

2018年母亲节的日期是5月13日。这个函数经常被调用,我们不想每次都重复计算,为此我们对函数稍稍加以改进,使其能计算任何年份的母亲节日期,并且通过缓存来提高性能。

function getMotherDayForYear(year) {
    let date = new Date(year, 4, 0);
    //4月份的最后一天是星期几。
    let day = date.getDay();
    //从4月份的最后一天开始计算,出现的第2个周日就是母亲节。
    //注意表示周几的数字是从0开始的。
    let secondSunday = 14 - day;
    date.setDate(secondSunday);
    return date;
}

const getMotherDayForYearM = f.memoize(getMotherDayForYear);

f.log(getMotherDayForYearM(2018));
//=> Fri Apr 13 2018 00:00:00 GMT+0800 (China Standard Time)

f.log(getMotherDayForYearM(2019));
//=> Fri Apr 12 2019 00:00:00 GMT+0800 (China Standard Time)

一切都很正常。直到有一天,一位来中国出差的蒙古程序员调用这个函数,他发现结果不正确,因为在蒙古母亲节是每年的6月1日(对,和儿童节同一天)。于是,他将日期稍作修改,用在自己的程序里。可是从此以后,他的中国同事再调用该函数,就发现返回的都是儿童节了。

//在中国出差的蒙古程序员调用该函数。
let date = getMotherDayForYearM(2018);
date.setMonth(5);
date.setDate(1);
f.log(date);
//=> Fri Jun 01 2018 00:00:00 GMT+0800 (China Standard Time)

//他的中国同事调用该函数。
f.log(getMotherDayForYearM(2018));
//=> Fri Jun 01 2018 00:00:00 GMT+0800 (China Standard Time)

问题就出在getMotherDayForYearM函数返回的日期保存在它的闭包里,而JavaScript中的Date对象是可变的,函数的用户对返回值的修改污染了缓存中的数据。这可以归咎于getMotherDayForYearM具有内部状态,不是纯函数,也可以理解为Date对象的setMonth等方法具有副作用,修改了相当于参数的Date对象。纠正的方法有很多,或者采用不带缓存的getMotherDayForYear函数,或者函数的用户注意不要修改返回的日期对象,比如只需要日期的年月日周等属性的情况下,可以读取这些属性值再进行计算,假如需要修改和使用整个日期对象,则可以先创建一个副本。


6.3.6  克隆和冻结
6.3.7  不可变的数据结构
6.3.8  不可变的映射和数组
6.3.9  不可变类型的其他好处
6.4  小结

更多内容,请参看拙著:

《JavaScript函数式编程思想》(京东)

《JavaScript函数式编程思想》(当当)

《JavaScript函数式编程思想》(亚马逊)

《JavaScript函数式编程思想》(天猫)

没有更多推荐了,返回首页