JavaScript函数式编程

298 阅读6分钟

为什么学习函数式编程

JavaScript具有函数式编程所需的重要特性:

  • 具有一等函数:具有使用函数作为参数值的能力:能够将函数作为参数传递,返回函数,并赋值给变量或对象。这种特性可以允许更高阶的函数,能够进行函数的组合、柯里化等。
  • 匿名函数以及具有简明的lambda语法:例如 x => x*2 是一个有效的 JavaScript 表达式。
  • 闭包:闭包是函数和声明该函数的词法环境的组合。当一个函数定义在另一个函数内时,即使外部函数已退出执,仍然可以访问外部函数定义的变量。

JavaScript是一种多范例语言,这意味着它支持许多不同样式的编程。但是多范式编程具有重要的缺点,那就是命令式和面向对象的编程方式会导致一切都是可变的,不确定的。例如以下情况:

const foo = {
    bar: 'abc'
};
foo.bar = 'def' // foo发生了改变

通常来说,对象都是可变的,以便它们的属性能够通过方法进行更新。在命令式编程中,大多数数据结构是可变的,以实现对对象和数组的有效操作。

以下是函数式语言所具有,而 JavaScript 不具有的一些特性:

  • 纯函数:在一些函数式编程语言中,纯函数是靠语言来实现的,不允许有副作用的表达式。
  • 不变性:一些函数式编程语言是不允许有突变的,表达式不再去改变现有的数据结构(对象以及变量),而是计算为新属性并返回。
  • 递归:递归是函数为了迭代的目的而调用自身的能力,在许多函数式编程语言中,递归是迭代的唯一方法。没有类似于 while 或 for 循环的循环语句。

纯函数

纯函数的特性

1、给定相同的输入,一定会返回相同的输出

// 输入相同的值,返回相同的结果
const highpass = (cutoff, value) => value >= cutoff;
highpass(5, 5); // => true
highpass(5, 5); // => true
highpass(11, 2); // => false
highpass(11, 2); // => false

// 以下代码不是纯函数
Math.random(); // => 0.4011148700956255
Math.random(); // => 0.8533405303023756
Math.random(); // => 0.3550692005082965

2、不会产生副作用,这意味着它不能改变任何外部状态

// 有副作用的函数
const addToCar = (car, item, quantity) => {
    cart.items.push({
        item,
        quantity
    });
return car;
};

// 原始对象
const originalCar = {
    items: []
};

const newCar = addToCar(
    originalCar,
    {
        name: 'BMW',
        price: '99'
    },
1
);

console.log(JSON.stringify(originalCar, undefined, 2));
// Log  可以看出addToCar方法改变了外部的originalCar对象
{
    'items': [
        {
            'item': {
                'name': 'BMW',
                'price': '99'
            },
            'quantity': 1
        }
    ]
}
// 没有副作用的函数
const addToCar = (car, item, quantity) => {
    return {
        car,
        items: car.items.concat([{
            item,
            quantity
        }]),
    };
    };
    
    const orignalCar = {
        items: []
    };
    
    const newCar = addToCar(
    originalCar,
    {
        name: 'BMW',
        price: '99'
    },
    1
    );
    
    console.log(
    JSON.stringify(originalCar, undefined, 2)
    );
    
    Logs:
    {
        'items': []
    }

3、可以用其结果值替换函数调用,而无需更改程序的含义

const addX = x => {x += 1}
myX = addX(2) // 3
// 等同于
myX = 3

纯函数具有许多有益的特性,是函数编程的基础。纯函数完全独立于外部状态,与外部可变状态导致的一些bug脱离关系。它们的独立性还使它们成为跨多个cpu和整个分布式计算集群的并行处理的最佳选择,这使得它们对于许多类型的科学和资源密集型计算任务至关重要。纯函数也是非常独立的,易于在代码中移动、复用、重组,使程序变得更加的灵活。

共享状态

什么是共享状态

共享状态是存在于共享作用域中的任何变量、对象或内存空间,或作为在作用域之间传递的对象的属性而存在的任何变量、对象或内存空间,共享作用域可以包括全局作用域或闭包作用域。

共享状态存在的问题

在开发中,我们经常会用到ajax进行数据请求,我想我们大部分开发者一定用到过输入框的输入联想查询,当用户输入过程中,每次输入都会触发一次请求,用来获取候选项数据,由于用户的输入速度往往快于API的响应速度,这边会导一些奇怪的问题,比如我们实际需要获取的最新数据被之前的旧数据所取代。这是因为最慢的一个ajax请求总是会取代最新的结果,从而导致一些不可预料的错误。如果输出取决于不可控制事件的顺序(如网络、设备),则会发生争用条件(等待时间、用户输入、随机性等)。虽然JavaScript是单线程运行的,但这并不代表着不存在并发性的情况。

const x = {
    val: 2
};

const x1 = () => x.val += 1;
const x2 = () => x.val *= 2;
x1();
x2();
// 将会改变外部x的值
console.log(x.val) // 6

我们可以重构以上代码:

const x = {
  val: 2
};
const x1 = x => ({...x, val: x.val + 1});
const x2 = x => ({...x, val: x.val * 2});
x1();
x2();
console.log(x2(x1(x)).val); // 6
console.log(x.val) // 2

一个函数的更改,或者一个函数的调用,不会影响程序的其他部分

不变性

不可变对象是创建后不能修改的对象。不可变是函数式编程的核心概念,因为没有它,程序中的数据流就会丢失,状态的历史记录将丢失,最后会导致一些不可预知的错误。 在 JavaScript 编程中,我们不能将 const 声明和不可变对象混淆。const创建的对象不可改变绑定的引用对象,但是你仍然可以改变引用对象的属性,这意味着const声明的对象是可变的。JavaScript有一个方法可以冻结一个一层级的对象:

const a = Object.freeze({
    foo: 'Hello',
    bar: 'world',
    baz: '!'
});

a.foo = 'GoodBye';
// Error: Cannot assign to read only property 'foo' of object Object

这个方法只能冻结第一层级的对象:

const a = Object.freeze({
    foo: { greeting:'Hello' },
    bar: 'world',
    baz: '!'
});

a.foo.greeting = 'GoodBye';
console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`); // 'Goodbye, world!'

我们可以通过深度遍历来冻结所有层级的对象,以下是两个实现冻结对象的函数库 Immutable.js Mori

副作用

副作用是在被调用函数之外可以观察到的任何应用程序状态的变化,而不是其返回值。副作用包括:

  • 修改任何外部变量或对象属性(例如,全局变量或父函数作用域链中的变量)
  • 登录控制台
  • 写入屏幕
  • 写入文件
  • 写入网络
  • 触发任何外部进程
  • 调用其它有副作用的函数