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

starrow starrow     2022-12-05     109

关键词:

第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函数式编程思想》(天猫)

《javascript函数式编程思想》

自序伴随着Web技术的普及,JavaScript已成为应用最广泛的编程语言之一。由于其在Web前端编程中的统治地位、语言本身的表现力、灵活性、开源的本质和ECMAScript标准近年来的快速发展,JavaScript向各个领域渗透的势头仍然... 查看详情

《javascript函数式编程思想》——部分应用和复合

第5章 部分应用和复合一等值的函数,是函数式编程的基石。部分应用和复合,则是函数式编程的重要特征。采用命令式编程时,每当我们感觉需要抽象出一个新的功能时,就会定义一个函数。在函数式编程中... 查看详情

《javascript函数式编程思想》——列表

第8章 列表函数式编程与列表处理有很深的渊源。列表是最基础,也是使用最普遍的复合数据类型。作为最早出现的函数式编程语言之一,Lisp【注:它的名称就来源于“列表处理器”(LIStProcessor)】用函数参... 查看详情

《javascript函数式编程思想》

自序伴随着Web技术的普及,JavaScript已成为应用最广泛的编程语言之一。由于其在Web前端编程中的统治地位、语言本身的表现力、灵活性、开源的本质和ECMAScript标准近年来的快速发展,JavaScript向各个领域渗透的势头仍然... 查看详情

《javascript函数式编程思想》——从面向对象到函数式编程

...假如本书的写作时间倒退回十年前,书名可能会变成JavaScript面向对象编程思想。自上世纪90年代兴起的面向对象编程思想随Java的繁荣达于顶点,在JavaScript从一门只被用来编写零星的简单的表单验证代码的玩具语言变成日... 查看详情

玩转javascript面试:何为函数式编程?(代码片段)

函数式编程在JavaScript领域着实已经成为一个热门话题。就在几年前,很多JavaScript程序员甚至都不知道啥是函数式编程,但是就在近三年里我看到过的每一个大型应用的代码库中都包含了函数式编程思想的大规模使用。函数式编... 查看详情

《javascript函数式编程思想》——函数是一等值

第4章 函数是一等值在函数式编程的标准或特点中,“函数是一等值”是最基本和重要的,也是最为人所知的,所有介绍函数式编程的书籍和文章都会优先介绍这一点,以至于“一等值”几乎成为函数的专属头衔&... 查看详情

《javascript函数式编程思想》——类型系统

第2章 类型系统为什么在许多编程语言中整数和浮点数是两种类型?结构体、数组、列表、映射……这些类型有什么关系?用户自定义的各种类型与它们又有什么关系?函数也是类型吗?强类型和弱类型意味着什... 查看详情

《javascript函数式编程思想》——递归

第7章 递归王二、张三和赵四一日无聊,决定玩击鼓传花讲冷笑话的游戏。王二和张三围成一圈传花,赵四负责击鼓。张三接连讲了几个诸如小菜、狐狸狡猾的笑话。花停在了王二的手中。王二:这个笑话很短。你要... 查看详情

javascript函数式编程(代码片段)

JavaScript函数式编程(一) JavaScript函数式编程(二)在第二篇文章里,我们介绍了 Maybe、Either、IO 等几种常见的Functor,或许很多看完第二篇文章的人都会有疑惑:『这些东西有什么卵用?』事实上,如果只是为了学... 查看详情

javascriptes6函数式编程:柯里化偏应用组合管道(代码片段)

上一篇介绍了闭包和高阶函数,这是函数式编程的基础核心。这一篇来看看高阶函数的实战场景。首先强调两点:注意闭包的生成位置,清楚作用域链,知道闭包生成后缓存了哪些变量高阶函数思想:以变量作用域作为根基,以... 查看详情

kotlin函数式编程思想fpinkotlin

Kotlin函数式编程思想:FPinKotlin函数式编程特性闭包和高阶函数函数编程支持函数作为第一类对象,有时称为闭包或者仿函数(functor)对象。实质上,闭包是起函数的作用并可以像对象一样操作的对象。与此类似,FP语言... 查看详情

函数响应式编程(frp)思想-callback风格

...影响a,这也就是所谓【变化传播】)函数式编程不产生副作用的函数称为纯函数 函数是一等公民=>高阶函数 函数式编程抽取了很多常用操作,作为高阶函数,比如map,filter,reduce。 有了这些函数,你的代码将被大... 查看详情

函数式编程思想

对于函数式编程来说,其只关心,定义输入数据和输出数据相关的关系,数学表达式里面其实是在做一种映射(mapping),输入的数据和输出的数据关系是什么样的,是用函数来定义的。http://www.yxtvg.com/toutiao/5413179/20180212a04ro500.ht... 查看详情

javascript函数式编程-包含闭包链式优化及柯里化

本文着重介绍个人理解的函数式编程。函数式编程个人理解为:以函数为主要载体的编程方式。好处:语义更加清晰可复用性高可维护性好作用域局限、副作用少基本函数式编程://实现数组中每个单词首字母大写//一般写法const... 查看详情

简单的 Node/Express 应用程序,函数式编程方式(如何在 JavaScript 中处理副作用?)

】简单的Node/Express应用程序,函数式编程方式(如何在JavaScript中处理副作用?)【英文标题】:SimpleNode/Expressapp,thefunctionalprogrammingway(Howtohandleside-effectsinJavaScript?)【发布时间】:2017-12-2420:50:52【问题描述】:关于JavaScript中的函... 查看详情

函数式编程思想:耦合和组合,第2部分

...者可能会对这一方法的缺点及其他的可选做法视而不见,函数式编程使用不同的构建块来实现重用,其基于的是更一般化的概念,比如说列表转换和可移植代码。函数式编程思想的这一部分内容比较了作为重用机制的经由继承的... 查看详情

函数响应式编程(frp)思想

...让你的代码像数学一样简洁,业务像流水一样清晰流畅。函数响应式编程响应式编程思想为体,函数式编程思想为用。响应式编程例如,在命令式编程环境中,a:=b+c表示将表达式的结果赋给a,而之后改变b或c的值不会影响a。但... 查看详情