为什么学习函数式编程
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
副作用
副作用是在被调用函数之外可以观察到的任何应用程序状态的变化,而不是其返回值。副作用包括:
- 修改任何外部变量或对象属性(例如,全局变量或父函数作用域链中的变量)
- 登录控制台
- 写入屏幕
- 写入文件
- 写入网络
- 触发任何外部进程
- 调用其它有副作用的函数