JavaScript函数式编程之副作用

126 阅读5分钟

什么是函数副作用?

副作用_是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互_。 函数副作用是指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。副作用的函数不仅仅只是返回了一个值,而且还做了其他的事情,比如:

  1. 修改了一个变量
  2. 直接修改数据结构
  3. 设置一个对象的成员
  4. 抛出一个异常或以一个错误终止
  5. 打印到终端或读取用户输入
  6. 读取或写入一个文件
  7. 在屏幕上画图

函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性,严格的函数式语言要求函数必须无副作用。 纯函数 ( Pure Function ) 输入输出

数据

流全是显式(Explicit)的。 显式(Explicit)的意思是,函数与外界交换数据只有一个唯一渠道——参数和返回值。函数从函数外部接受的所有输入信息都通过参数传递到该函数内部。函数输出到函数外部的所有信息都通过返回值传递到该函数外部。

非纯函数 ( Impure Function ) 与之相反。 隐式(Implicit)的意思是,函数通过参数和返回值以外的渠道,和外界进行数据交换。比如读取/修改全局变量,都叫作以隐式的方式和外界进行数据交换。

引用透明 ( Referential Transparent ) 引用透明的概念与函数的副作用相关,且受其影响。 如果程序中两个相同值得表达式能在该程序的任何地方互相替换,而不影响程序的动作,那么该程序就具有引用透明性。它的优点是比非引用透明的语言的语义更容易理解,不那么晦涩。纯函数式语言没有变量,所以它们都具有引用透明性。

以下示例说明了引用透明与函数副作用的结合,代码如下:

result1 = (fun(a) + b) / (fun(a) -c); 
temp = fun(a); 
result2 = (temp + b) / (temp -c);

如果函数没有副作用,那么result1和result2将是等价的。然而如果fun有副作用,比如让b或c加1,那么result1和result2将不相等。因此,副作用违背了引用透明性。 在JavaScript中,引入了函数。但显然js中的函数可以访问、修改全局变量(或定义在函数外的变量),如下

var a = 5; 
function fun(){ 
a = 10; 
} 
fun(); // a 变成了10

js中要想保证函数无副作用这项特性,只能依靠编程人员的习惯,即:

  1. 函数入口使用参数运算,而不修改它
  2. 函数内不修改函数外的变量,如全局变量
  3. 运算结果通过函数返回给外部(出口)

上文中的纯函数的概念很严格,这个副作用的概念也是。它的要求很高,概括的讲,只要是跟函数外部环境发生的交互就都是副作用。从“副作用”这个词语来看,它更多的情况在于“改变系统状态”。

在教程中列举的一些副作用:

更改文件系统 往数据库插入记录 发送一个http请求 可变数据 打印/log 获取用户输入 DOM查询 访问系统状态 如果完全没有副作用,那我们的代码就是单纯的跑一遍浪费了一点电而已,除此之外什么都没有发生,这样的话我们写代码就没有意义了。所以,在JS中,我们的目的不是完全消除副作用注1,而是避免那些不应该出现的副作用。

JS原生的方法中,map就很函数式,他会返回一个新的数组,不会改变原数组。而pop这种方法就很不好,它在操作了数组之后,也改变数组本身。

所以当我们要使用那些有副作用的方法写纯函数的时候,记得做一次深拷贝: 例1

const myPop = x => {
  let [...y] = x;
  return y.pop();
} 

使用一个固定的共享状态或者调用一个纯函数不算是副作用,例子如下:

例2

const a = 5;
function A(b) {
  return a + b;
}
A(5); 

调用纯函数的例子:

例3

function foo(x) {
  return bar(x);
}
 
function bar(y) {
  return y + 1;
}
 
foo(1); 

虽然不算是副作用,可更加推荐的方式是把函数bar用参数的方式传进来,这样就做到了解耦,用起来更加的方便:

例4

function foo(fn, x) {
  return fn(x);
}
 
function bar(y) {
  return y + 1;
}
 
foo(bar, 1); 

如果使用柯里化的方式,会更加的清爽和方便:

例5

function foo(fn) {
  return function(x) {
    return fn(x);   
  }
}
 
function bar(y) {
  return y + 1;
}
 
foo(bar)(1); 

这个例子依然存在一个会令我们感到不安的地方,那就是bar可能会被修改。例如:

例6

function foo(fn, x) {
  return fn(x);
}
 
function bar(y) {
  return y + 1;
}
bar = undefined;
foo(bar, 1); 

当然我们平时很少会大脑抽筋在全局作用域下写出一个bar = undefined来让我们的系统出错,这更可能在某个有副作用的函数内出现这种情况。这就是为什么我们要避免副作用。这个情况在ES6中会得到改善,例如:

例7

const foo = function(fn, x) {
  return fn(x);
}
 
const bar = function(y) {
  return y + 1;
}
bar = undefined;	// error
foo(bar, 1); 

个人建议用const的方式,这样更加的安全,即便出错也可以快速定位。

注释:

注1: 如果继续深入学习,对与上面列出的一些副作用,函数式还有一种延迟执行的方式(IO容器)来使这些操作变纯。

注:以上内容部分来自转载,仅个人学习记录使用