Javascript中的函数式编程

所谓的函数式编程,网上有很多种的看法,其主要有以下几个方面

  • 与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)并列的编程范式
  • 函数是第一位的。
  • 强调将计算过程分解成可复用的函数,典型例子就是map方法和reduce方法组合而成 MapReduce 算法。
  • 只有纯的、没有副作用的函数,才是合格的函数。

本文尝试通过一些小示例更好的理解函数式编程

不可变性(Immutability)

首先,让我们考虑以下对象

1
2
3
4
let person = {
name:'mike',
age:24
}

我们创建一个函数,主要功能是改变person对象的用户名,

1
2
3
4
5
6
function changeName(person, name) {
person.name = name
return person
}
console.log(changeName(person, 'Jhon').name) // Jhon
console.log(person.name) //Jhon

在Javascript中,函数参数是对实际对象的引用,更改对象,就会影响到实际的对象。在这里,更改函数中person对象就是更原对象。
函数式编程中一条重要的原则就是不变性,也就是不能改变原对象。所以,我们对函数进行改造:

1
2
3
4
5
var changeName = function(person, name) {
return Object.assign({}, person, {name:name})
}
console.log(changeName(person,'Jhon').name) // Jhon
console.log(person.name) //mike

这里,我们使用的是Object.assign函数,Object.assign函数用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,完成后它将返回目标对象。
进一步,我们可以根据ES6的一些特性简化代码

1
2
3
4
const changeName = (person,name)=>({
...person,
name
})

注意: 箭头函数不能指向一个对象,所以对象外必须包裹一个大括号

我们进一步的考虑下面的对象数组

1
2
3
4
5
let list = [
{ title: "Rad Red"},
{ title: "Lawn"},
{ title: "Party Pink"}
]

我们能够通过Array.push函数添加颜色到数组

1
2
3
4
5
6
var addColor = function(title, colors) {
colors.push({ title: title })
return colors;
}
console.log(addColor("Glam Green", list).length) //4
console.log(list.length) //4

但是,需要注意的是,Array.push函数不是一个不可变性函数,因为对Array.push函数的使用会影响原数据。
所以,为了保证数据的不变性,我们需要使用一个不变性的函数Array.concat

1
2
3
const addColor = (title, array) => array.concat({title})
console.log(addColor("Glam Green", list).length) //4
console.log(list.length) //3

利用ES6的特性进一步简化

1
const addColor = (title, list) => [...list, {title}]

这样,addColor的主要功能就是保存一个对象数组副本,然后在此对象副本上操作,其结果不影响原数据

纯函数(Pure Functions)

所谓的纯函数就是基于一个参数返回一个值(值可以是函数)。(纯函数至少需要一个函数和一个返回值)

1
2
3
4
5
6
7
8
9
10
11
12
13
let notebook = {
title:'notebook',
canRead:false,
canWrite:false
}

function reverseAccess(){
notebook.canWrite = true
notebook.canRead = true
}
reverseAccess()
console.log(notebook)
//{ name:'notebook',canRead:true,canWrite:true}

reverseAcces不是一个纯函数,因为他没有任何参数,也没有返回任何值。它仅仅改变了一个外在变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let notebook {
title:'notebook',
canRead:false,
canWrite:false
}

const reverseAccess = (person) =>{
notebook.canWrite = true
notebook.canRead = true
return person
}
reverseAccess(notebook)
console.log(notebook)
//{ name:'notebook',canRead:true,canWrite:true}
//{ name:'notebook',canRead:true,canWrite:true}

和上面一样,虽然reverseAccess接受了一个参数,并且返回了一个参数。
但我们考虑上文的不可变性,一个纯函数不应该影响原数据(或者说参数的对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let notebook {
title:'notebook',
canRead:false,
canWrite:false
}

const reverseAccess = (person) =>({
...person,
canRead:true
canWrite:true
})
reverseAccess(notebook)
console.log(notebook)
//{ name:'notebook',canRead:true,canWrite:true}
//{ name:'notebook',canRead:false,canWrite:false}

数据转换(Data Transformations)

既然在函数式编程中,数据的不可变性是十分重要的,那么我们如何对数据进行转换呢?在Javascript中,就有两个核心函数Array.mapArray.reduce来进行相关的操作。笔者在这一部分,会根据Array.mapArray.reduce等核心函数举几个示例来解释如何进行数据的转换

join 函数

1
2
3
4
5
6
7
const schools = [
"Yorktown",
"Washington & Lee",
"Wakefield"
]
console.log( schools.join(", ") )
// "Yorktown, Washington & Lee, Wakefield"

filter函数:

1
2
3
const wSchools = schools.filter(school => school[0] === "W")
console.log( wSchools )
// ["Washington & Lee", "Wakefield"]

map函数

1
2
3
4
5
6
7
const highSchools = schools.map(school => ({ name: school }))
console.log( highSchools )
//[
// { name: "Yorktown" },
// { name: "Washington & Lee" },
// { name: "Wakefield" }
// ]

reduce 函数

1
2
3
4
5
6
7
8
9
10
const ages = [21,18,42,40,64,63,34];
const maxAge = ages.reduce((max, age) => {
console.log(`${age} > ${max} = ${age > max}`);
if (age > max) {
return age
} else {
return max
}
}, 0)
console.log('maxAge', maxAge);

高阶函数(Higher-Order Functions)

高阶函数是将函数作为参数或者讲函数作为返回值的函数,在函数式编程中十分重要Array.map , Array.filter ,和 Array.reduce都可以接受函数作为参数

下面的userLogs展示了闭包技术,是一个高阶函数,且封装了userName,userLogs(“grandpa23”)返回一个函数。getFakeMembers(20).then接受两个函数作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
const userLogs = userName => message =>
console.log(`${userName} -> ${message}`)
const log = userLogs("grandpa23")

log("attempted to load 20 fake members")
getFakeMembers(20).then(
members => log(`successfully loaded ${members.length} members`),
error => log("encountered an error loading members")
)
// grandpa23 -> attempted to load 20 fake members
// grandpa23 -> successfully loaded 20 members
// grandpa23 -> attempted to load 20 fake members
// grandpa23 -> encountered an error loading members

递归(Recursion)

递归函数指的是是直接或间接调用函数本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const countdown = (value, fn) => {
fn(value)
return (value > 0) ? countdown(value-1, fn) : value
}
countdown(10, value => console.log(value));
// 10
// 9
// 8
// 7
// 6
// 5
// 4
// 3
// 2
// 1
// 0

注意: 递归需要注意递归层数

构成(Composition)

函数式编程的目标就是将程序分解为更小的函数,在不同的条件下,你可以自由的复用他们。
你可以自由的组合不同的函数以实现不同的功能

1
2
3
4
5
6
const template = "hh:mm:ss tt"
const clockTime = template.replace("hh", "03")
.replace("mm", "33")
.replace("ss", "33")
.replace("tt", "PM")
console.log(clockTime)

在这个示例中,每个replace函数都实现特定的功能,将他们组合在一起就实现了更改模板字符串的功能,在更复杂的情况下,每一个replace函数是可以复用的。
考虑下面的示例,其中civilianHours以Date为参数并返回一个Dated对象,appendAMPM以此为参数

1
const both = date => appendAMPM(civilianHours(date))

我们可以更优雅的实现,以实现函数的复用

1
2
3
4
5
6
7
8
9
10
11
12
const compose = (...fns) =>
(arg) =>
fns.reduce(
(composed, f) => f(composed),
arg
)

const both = compose(
civilianHours,
appendAMPM
)
both(new Date())

参考 (Reference)

函数式编程入门教程

Learning React