本篇会将谈谈函数编程中一个很重要的细节 —— “副作用”。
维基上关于副作用的解释:
函数内部有隐式(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 ) 的结果却不一样,又增大了阅读的负担。相信我,这是个最简单抽象的例子,实际的影响将远大于此。
避免副作用?
- const
以上面的例子来说:这样写,foo( 1 ) 的结果当然是确定的,因为用到了 const 来固定外部变量。
const y = 5;
// ..
foo( 1 );
- I/O
一个没有 I/O 的程序是完全没有意义的,因为它的工作不能以任何方式被观察到。一个有用的程序必须最少有一个输出,并且也需要输入。输入会产生输出。
还记得 foo(..) 函数片段 2 吗?没有输出 return,这是不太可取的。
// 片段 2
function foo(x) {
y = x * 2;
}
var y;
foo( 3 );
- 明确依赖
我们经常会由于函数的异步问题导致数据出错;一个函数引用了另外一个函数的回调结果,当我们作这种引用时要特别注意。
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 需要前者的回调;
写出有副作用/效果的代码是很正常的, 但我们需要谨慎和刻意地避免产生有副作用的代码。
- 运用幂等
这是一个很新但重要的概念!
从数学的角度来看,幂等指的是在第一次调用后,如果你将该输出一次又一次地输入到操作中,其输出永远不会改变的操作。
一个典型的数学例子是 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(..) 方法等,看起来安全,但实际上会修改数组本身。我们需要复制一个变量来解耦(深拷贝)。
函数的纯度是和自信是有关的。函数越纯洁越好。制作纯函数时越努力,当您阅读使用它的代码时,你的自信就会越高,这将使代码更加可读。
其实,关于函数纯度还有更多有意思的点:
思考一个问题,如果我们把函数和外部变量再封装为一个函数,外界无法直接访问其内部,这样,内部的函数算不算是一个纯函数?
假如一棵树在森林里倒下而没有人在附近听见,它有没有发出声音?
阶段小结
- 我们反复强调:开发人员喜欢显式输入输出而不是隐式输入输出。
- 如果有隐式的输入输出,那么就有可能产生副作用。
- 有副作用的代码让我们的代码理解起来更加费劲!
- 解决副作用的方法有:定义常量、明确 I/O、明确依赖、运用幂等……
- 在 js 运用幂等是一个新事物,我们需要逐渐熟悉它。
- 没有副作用的函数就是纯函数,纯函数是我们追求编写的!
- 将一个不纯的函数重构为纯函数是首选。但是,如果无法重构,尝试封装副作用。(假如一棵树在森林里倒下而没有人在附近听见,它有没有发出声音?—— 有没有其实已经不重要了,反正听不到)
以上,便是本次关于 JS 函数式编程 副作用 这个细节的讲解。
这个细节,真的很重要!
文章版权声明
1 原创文章作者:3979,如若转载,请注明出处: https://www.52hwl.com/36385.html
2 温馨提示:软件侵权请联系469472785#qq.com(三天内删除相关链接)资源失效请留言反馈
3 下载提示:如遇蓝奏云无法访问,请修改lanzous(把s修改成x)
4 免责声明:本站为个人博客,所有软件信息均来自网络 修改版软件,加群广告提示为修改者自留,非本站信息,注意鉴别