XDM,JS如何函数式编程?看这就够了!(四)

简介: 本篇会将这个蓝图再具象一下,谈谈函数编程中一个很重要的细节 —— “副作用”。

image.png

不知不觉,我们已经来到了《JS如何函数式编程》系列的【第四篇】。


前三篇传送门:


经过前几篇的历练,本瓜相信你的心中一定对函数编程有了基本的蓝图。


本篇会将这个蓝图再具象一下,谈谈函数编程中一个很重要的细节 —— “副作用”

  • 点赞富三代👍👍👍评论美一生🎉🎉🎉


维基上关于副作用的解释:

函数内部有隐式(Implicit)的数据流,这种情况叫做副作用(Side Effect)。


咱们前文也提到过:开发人员喜欢显式输入输出而不是隐式输入输出。

所以我们将细致的看看副作用中【隐式】和【显式】的区别!


何为副作用?



先来个小例子作开胃菜:

// 片段 1
function foo(x) {
    return x * 2;
}
var y = foo( 3 );
// 片段 2
function foo(x) {
    y = x * 2;
}
var y;
foo( 3 );


片段 1 和片段 2 实现的最终效果是一致的,即 y = 3 * 2 ,但是片段 1 是显示的,片段 2 是隐式的。


原因是:片段 2 在函数内引用了外部变量 y。

片段 2 ,当我们调用 foo( 3 ) 时,并不知道其内部是否会修改外部变量 y。它的修改是隐式的,即产生了副作用!


有副作用的函数可读性更低,我们需要更多的阅读来理解程序。


再举一例:

var x = 1;
foo();
console.log( x );
bar();
console.log( x );
baz();
console.log( x );


如果每个函数内都引用了 x ,有可能对其赋值修改,那么我们很难知道每一步 x 的值是怎样的,要每一步去追踪!


选择在一个或多个函数调用中编写带有(潜在)副作用的代码,那么这意味着你代码的读者必须将你的程序完整地执行到某一行,逐步理解。


如果 foo()bar()、和 baz() 这三个函数没有(潜在)副作用,x 的值一眼可见!


一定是修改外部变量才是产生副作用了吗?

function foo(x) {
    return x + y;
}
var y = 3;
foo( 1 ); 


这段代码中,我们没有修改外部变量 y ,但是引用了它,也是会产生副作用的。


y = 5;
// ..
foo( 1 );   

两次 foo( 1 ) 的结果却不一样,又增大了阅读的负担。相信我,这是个最简单抽象的例子,实际的影响将远大于此。


避免副作用?



  1. const


以上面的例子来说:这样写,foo( 1 ) 的结果当然是确定的,因为用到了 const 来固定外部变量。


const y = 5;
// ..
foo( 1 );


  1. I/O


一个没有 I/O 的程序是完全没有意义的,因为它的工作不能以任何方式被观察到。一个有用的程序必须最少有一个输出,并且也需要输入。输入会产生输出。


还记得 foo(..) 函数片段 2 吗?没有输出 return,这是不太可取的。

// 片段 2
function foo(x) {
    y = x * 2;
}
var y;
foo( 3 );


  1. 明确依赖

我们经常会由于函数的异步问题导致数据出错;一个函数引用了另外一个函数的回调结果,当我们作这种引用时要特别注意。


var users = {};
var userOrders = {};
function fetchUserData(userId) {
    ajax( "http://some.api/user/" + userId, function onUserData(userData){
        users[userId] = userData;
    } );
}
function fetchOrders(userId) {
    ajax( "http://some.api/orders/" + userId, function onOrders(orders){
        for (let i = 0; i < orders.length; i++) {
                // 对每个用户的最新订单保持引用
            users[userId].latestOrder = orders[i];
            userOrders[orders[i].orderId] = orders[i];
        }
    } );
}


fetchUserData(..) 应该在 fetchOrders(..) 之前执行,因为后者设置 latestOrder 需要前者的回调;

写出有副作用/效果的代码是很正常的, 但我们需要谨慎和刻意地避免产生有副作用的代码。


  1. 运用幂等


这是一个很新但重要的概念!


从数学的角度来看,幂等指的是在第一次调用后,如果你将该输出一次又一次地输入到操作中,其输出永远不会改变的操作。


一个典型的数学例子是 Math.abs(..)(取绝对值)。Math.abs(-2) 的结果是 2,和 Math.abs(Math.abs(Math.abs(Math.abs(-2)))) 的结果相同。


幂等在 js 中的表现:

// 例 1
var x = 42, y = "hello";
String( x ) === String( String( x ) );                // true
Boolean( y ) === Boolean( Boolean( y ) );            // true
// 例 2
function upper(x) {
    return x.toUpperCase();
}
function lower(x) {
    return x.toLowerCase();
}
var str = "Hello World";
upper( str ) == upper( upper( str ) );                // true
lower( str ) == lower( lower( str ) );                // true
// 例 3
function currency(val) {
    var num = parseFloat(
        String( val ).replace( /[^\d.-]+/g, "" )
    );
    var sign = (num < 0) ? "-" : "";
    return `${sign}$${Math.abs( num ).toFixed( 2 )}`;
}
currency( -3.1 );                                    // "-$3.10"
currency( -3.1 ) == currency( currency( -3.1 ) );    // true


实际上,我们在 js 函数式编程中幂等有更加宽泛的概念,即只用要求:f(x) === f(f(x))


// 幂等的:
obj.count = 2; // 这里的幂等性的概念是每一个幂等运算(比如 obj.count = 2)可以重复多次
person.name = upper( person.name );
// 非幂等的:
obj.count++;
person.lastUpdated = Date.now();
// 幂等的:
var hist = document.getElementById( "orderHistory" );
hist.innerHTML = order.historyText;
// 非幂等的:
var update = document.createTextNode( order.latestUpdate );
hist.appendChild( update );


我们不会一直用幂等的方式去定义数据,但如果能做到,这肯定会减少意外情况下产生的副作用。这需要时间去体会,我们就先记住它。


纯函数



你应该听说过纯函数的大名,我们把没有副作用的函数称为纯函数。

例 1:

function add(x,y) {
    return x + y;
}


输入(x 和 y)和输出(return ..)都是直接的,没有引用自由变量。调用 add(3,4) 多次和调用一次是没有区别的。add(..) 是纯粹的编程风格的幂等。


例 2:

const PI = 3.141592;
function circleArea(radius) {
    return PI * radius * radius;
}


circleArea 也是纯函数。虽然它调用了外部变量 PI ,但是 PI 是 const 定义的常量,引用常量不会产生副作用;


例 3:

function unary(fn) {
    return function onlyOneArg(arg){
        return fn( arg );
    };
}


unary 也是纯函数。


表达一个函数的纯度的另一种常用方法是:给定相同的输入(一个或多个),它总是产生相同的输出。


不纯的函数是不受欢迎的!因为我们需要更多的精力去判断它的输出结果!

写纯函数需要更多耐心,比如我们操作数组的 push(..) 方法,或 reverse(..) 方法等,看起来安全,但实际上会修改数组本身。我们需要复制一个变量来解耦(深拷贝)。

函数的纯度是和自信是有关的。函数越纯洁越好。制作纯函数时越努力,当您阅读使用它的代码时,你的自信就会越高,这将使代码更加可读。


其实,关于函数纯度还有更多有意思的点:

思考一个问题,如果我们把函数和外部变量再封装为一个函数,外界无法直接访问其内部,这样,内部的函数算不算是一个纯函数?


假如一棵树在森林里倒下而没有人在附近听见,它有没有发出声音?


阶段小结



  1. 我们反复强调:开发人员喜欢显式输入输出而不是隐式输入输出。
  2. 如果有隐式的输入输出,那么就有可能产生副作用。
  3. 有副作用的代码让我们的代码理解起来更加费劲!
  4. 解决副作用的方法有:定义常量、明确 I/O、明确依赖、运用幂等......
  5. 在 js 运用幂等是一个新事物,我们需要逐渐熟悉它。
  6. 没有副作用的函数就是纯函数,纯函数是我们追求编写的!
  7. 将一个不纯的函数重构为纯函数是首选。但是,如果无法重构,尝试封装副作用。(假如一棵树在森林里倒下而没有人在附近听见,它有没有发出声音?—— 有没有其实已经不重要了,反正听不到)


以上,便是本次关于 JS 函数式编程 副作用 这个细节的讲解。

这个细节,真的很重要!

我是掘金安东尼,公众号【掘金安东尼】,输出暴露输入,技术洞见生活!


相关文章
|
前端开发 JavaScript 数据处理
深入学习JavaScript ES8函数式编程:特性与实践指南
深入学习JavaScript ES8函数式编程:特性与实践指南
85 0
|
5月前
|
存储 JavaScript 前端开发
JavaScript——函数式编程Functor(函子)
JavaScript——函数式编程Functor(函子)
30 0
|
7月前
|
前端开发 JavaScript 开发者
函数式编程在JavaScript中的应用
【6月更文挑战第10天】本文探讨了函数式编程在JavaScript中的应用,介绍了函数式编程的基本概念,如纯函数和不可变数据。文中通过示例展示了高阶函数、不可变数据的使用,以及如何编写纯函数。此外,还讨论了函数组合和柯里化技术,它们能提升代码的灵活性和可重用性。掌握这些函数式编程技术能帮助开发者编写更简洁、可预测的JavaScript代码。
|
8月前
|
JavaScript 前端开发
JavaScript 的数组方法 map()、filter() 和 reduce() 提供了函数式编程处理元素的方式
【5月更文挑战第11天】JavaScript 的数组方法 map()、filter() 和 reduce() 提供了函数式编程处理元素的方式。map() 用于创建新数组,其中元素是原数组元素经过指定函数转换后的结果;filter() 则筛选出通过特定条件的元素生成新数组;reduce() 将数组元素累计为单一值。这三个方法使代码更简洁易读,例如:map() 可用于数组元素乘以 2,filter() 用于选取偶数,reduce() 计算数组元素之和。
53 2
|
8月前
|
JavaScript 前端开发 测试技术
JavaScript中的函数式编程:纯函数与高阶函数的概念解析
【4月更文挑战第22天】了解JavaScript中的函数式编程,关键在于纯函数和高阶函数。纯函数有确定输出和无副作用,利于预测、测试和维护。例如,`add(a, b)`函数即为纯函数。高阶函数接受或返回函数,用于抽象、复用和组合,如`map`、`filter`。函数式编程能提升代码可读性、可维护性和测试性,帮助构建高效应用。
|
8月前
|
JavaScript 前端开发 索引
JavaScript函数式编程【进阶】
JavaScript函数式编程【进阶】
61 1
|
8月前
|
存储 JavaScript 前端开发
JavaScript函数式编程[入门]
JavaScript函数式编程[入门]
59 1
|
8月前
|
前端开发 JavaScript 数据处理
深入学习JavaScript ES8函数式编程:特性与实践指南
深入学习JavaScript ES8函数式编程:特性与实践指南
125 0
|
缓存 JavaScript 前端开发
带你读《现代Javascript高级教程》十四、JavaScript函数式编程(1)
带你读《现代Javascript高级教程》十四、JavaScript函数式编程(1)
|
JavaScript 前端开发 测试技术
带你读《现代Javascript高级教程》十四、JavaScript函数式编程(2)
带你读《现代Javascript高级教程》十四、JavaScript函数式编程(2)
下一篇
开通oss服务