JavaScript的函数式编程笔记

in Notes with 0 comment

终于抽出时间去做简单的整理,内容比较基础,主要是颗粒化了知识点,方便日后查找加深理解,算是笔记吧

函数式编程

简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。

函数式编程属于声明式编程(declarative programming)的范畴,经常跟声明式编程一块儿讨论的是命令式编程(imperative programming)。

声明式编程

声明式编程一般是说声明(说明)一下你想要的结果是什么样的,然后返回给你想要的结果。

例如现在有一组成绩单,我要过滤成绩 90 分以上的学生,如下

let reports = [
  { name: 'Tom', grade: 86 },
  { name: 'Amy', grade: 94 },
  { name: 'Mike', grade: 85 },
  { name: 'Jack', grade: 79 },
  { name: 'Nami', grade: 91 }
]

声明式,如下

const declarativeReportFilter = (reports) => {
  return reports.filter((report) => report.grade >= 90)
}

在上面这个函数里,只是说明了一下,自己想要的结果是什么样的,就是成绩在 90 分以上的学生。

命令式编程

命令式编程一般是说清楚具体要怎么样得到一个结果:先这样做,再这样做,然后再这样,如果这样,就这样做 ...

还是原来的一组成绩单,命令式,如下

命令式:

const imperativeReportFilter = (reports) => {
  let result = []
  for (let i = 0; i < reports.length; i++) {
    if (reports[i].grade >= 90) {
      result.push(reports[i])
    }
  }
  return result
}

在函数里面,声明一个 result 变量,然后赋值到一个空白数组。然后通过 for 循环,去处理 reports ,循环的时候判断当前项目里的评分(grade)是不是大于等于 90,如果是的话,就把这个项目放到 result 里面。循环完成以后,会返回处理之后的结果。

命令式编程里,详细的说明了得到结果需要做的每个操作。

定义函数

在 JavaScript 语言里,函数是一种对象,所以可以说函数是 JavaScript 里的一等公民(first-class citizens)。所谓"一等公民",指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

定义一个函数:

function greet(greeting, name) {
  return `${greeting}, ${name}`
}

因为在 JavaScript 里面是对象(object),所以它会有一些属性还有方法。比如 name 属性是函数的名字,length 属性指的是函数里面有多少个必须要传递的参数。比如访问上面定义的这个函数里的两个属性:

greet.name
// 输出 greet

greet.length
// 输出 2

函数表达式

函数是对象,也可以说函数是一个值,在 JavaScript 里面,它跟其它类型的值是一样的,比如字符串,数字。这就可以让我们使用函数表达式的方法来定义函数,也就是定义一个匿名函数(anonymous function),再把它交给一个变量。如下:

var greet = function (greeting, name) {
  return `${greeting}, ${name}`
}

Lambda 表达式

ES6 可以让我们用 Lambda 表达式,也就是箭头函数(fat arrow function),如下:

var greet = (greeting, name) => {
  return `${greeting}, ${name}`
}

让它更简洁一些:

var greet = (greeting, name) => `${greeting}, ${name}`

箭头右边的东西会自动被返回(return)。

对象里的方法

如果有一个函数是在一个对象里,一般我们称这种函数是对象的一个方法(method)。

试一下:

var obj = {
  greet: function (greeting, name) {
    return `${greeting}, ${name}`
  }
}

ES6 可以让我们这样为对象定义方法:

var obj = {
  greet (greeting, name) {
    return `${greeting}, ${name}`
  }
}

上面定义了一个名字是 obj 的对象,在它里面添加了一个叫 greet 的方法。要使用这个方法可以这样:

obj.greet('hi', '😙')

// 返回 “ hi, 😙 ”

纯函数

函数式编程鼓励我们多创建纯函数(pure functions),纯函数只依赖你交给它的东西,不使用任何函数以外的东西,也不会影响到函数以外的东西。跟纯函数对应的就是不纯函数(impure functions),也就是不纯函数可能会使用函数以外的东西,比如使用了一个全局变量。也可能会影响到函数以外的东西,比如改变了一个全局变量的值。

多使用纯属函数是因为它更可靠一些,也没什么副作用(side effects)。你交给它同样的值,它每次都会给你输出同样的结果,这种特质叫所指透明(Referential transparency) 。这会让程序更稳定,也更容易测试。

副作用

纯函数没副作用,有副作用的函数都不纯。我吃了一片感冒药,是要治我的感冒,但副作用是它让我想睡觉。函数的副作用多数表现为函数依赖或者改变了它以外的东西。

let name = 'chakhsu'
const greet = () => {
  console.log(`hello, ${name}`)
}

greet 不是纯函数,因为这个函数依赖函数以外的东西,这里就是全局作用域下的 name。这样做的问题是,函数依赖的 name 很可能在应用运行的时候发生变化,这样试一下:

greet() // 输出:“hello, chakhsu”
let name = 'linpx' // name 的值被改变了
greet() // 输出:“hello, linpx”

这样改一下:

const greet = (name) => {
  console.log(`hello, ${name}`)
}

现在函数明确的说明了自己需要的东西,这里就是 name 参数。它现在只依赖你交给它的 name 参数的值。但是这个函数仍然不是纯函数,因为它在控制台上输出了东西,这其实改变了函数之外的东西,所以它不是纯函数。这样再改一下:

const greet = (name) => {
  return `hello, ${name}`
}

现在 greet 就会是一个纯函数,因为它只依赖交给它的 name ,也没有改变函数以外的东西。而且你每次给它同样的 name 值,它每次都会给我们返回同样的结果。这种函数用起来即安全又可靠。

所指透明

所指透明(Referential transparency)。比如我说:“中国的首都”。我的表达所指的意思就是 “北京”,没什么其它的隐含的意思。所以可以说我的表达所指是透明的(Referentially transparent)。再比如:“我有点饿”。这个表达所指就不透明,我表达的到底是什么意思是不能确定的,我可能是想出去吃点东西,也可能是想让你帮我买点东西回来吃。

纯函数所指的东西都是透明的,因为你给它同样的东西,它每次都会返回一样的结果

const greet = (name) => {
  return `hello, ${name}`
}

const logger = (message) => {
  console.log(message)
}

//运行结果一样
logger(greet('chakhsu'))
logger('hello, chakhsu')

因为 greet 所指透明,所以如果我们在表达式中把它替换成它所指的东西,不会影响到程序的运行。比如在一个表达式里所有使用 greet('chakhsu') 的地方,我们都可以把 greet('chakhsu') 替换成 hello, chakhsu ,这是因为 greet('chakhsu') 所指的东西就是字符串 hello, chakhsu

不变性

不变性(immutability),指的是一个东西被创建以后就不会发生变化了。函数式编程里的东西一般都具有这种特性。让对象具有不变性是一件好事,如果对象需要发生改变,你应该去创建一个新的对象,在新的对象里包含发生改变的部分,而不是直接去修改对象本身。

在 JavaScript 里面,字符串与数字都具有不变性。也就是一个字符串一旦被创建,它的值就不会发生变化。但是 JavaScript 里的数组或对象是可变的,让它们不可变可以使用 Object.freeze 冻结一下它们(只能冻一层),也可以使用一些外部库,比如 Immutable.js

实验一下:

let name = 'linpx.com'

name[5]
"."

name[5] = '-'
"-"

name
"linpx.com"

在上面尝试修改 name 的值(把 linpx.com 里的点“ . ” 换成 “ - ” ),办不到。因为字符串这种值不能被改变。

再试一下:

let names = ['Tom', 'Amy', 'Mike', 'Jack']

names[0] = 'Tomii'
"Tomii"

names
['Tomii', 'Amy', 'Mike', 'Jack']

names.push('Nami')
5

names
['Tomii', 'Amy', 'Mike', 'Jack', 'Nami']

这次我们试着改变一个数组里的值,在 JavaScript 里,这是被允许的,我们改变了原数组(names)的值。数组的一些方法也会改变原数组的值,比如我们用了 push 方法,在 names 里面添加了一个新项目,这个动作修改了原数组 names 的值。

Object.freeze

Object.freeze 可以冻结对象,但只能冻一层。

实验一下:

let names = ['Tom', 'Amy', 'Mike', 'Jack']

Object.freeze(names)
['Tom', 'Amy', 'Mike', 'Jack']

names[0] = 'Tomii'
"Tomii"

names
['Tom', 'Amy', 'Mike', 'Jack']

names.push('Nami')
VM705:1 Uncaught TypeError: Can't add property 4, object is not extensible

names
['Tom', 'Amy', 'Mike', 'Jack']

Object.freeze 冻结了一下创建的 names 数组,当要去修改 names 数组的时候就不会影响到 names 原始的值了。

柯里化

之前写过两篇关于柯里化的文章(传送门1传送门2),这里再简单补充一下

Currying 指的就是把一个接受多个参数的函数,搞成每次只接收一个参数的函数序列。

看个例子:

const greet = (greeting, name) => {
  return `${greeting}, ${name}`
}

greet('hello', 'chakhsu') // “hello, chakhsu”

上面的 greet 就是一个接收多个参数的函数。如果把它转换成 Currying 风格的函数,会像这样:

const greet = greeting => name => `${greeting}, ${name}`

上面用了箭头函数,如果写成普通的函数应该像这样:

const greet = function(greeting) {
  return function(name) {
    return `${greeting}, ${name}`
  }
}

greet 是一个函数,它接收一个参数是 greeting,这个 greet会返回一个函数,这个被返回的函数又会接收另一个参数:name,在这个被返回的函数里,会返回最终的结果。这里返回的就是把 greetingname 参数的值组织成了一个新的字符串。

这种函数用起来像这样:

greet('hello')('chakhsu') // “hello, chakhsu”

高阶函数

在 JavaScript 里面,函数跟普通的对象没啥大区别,所以你可以让函数作为参数传递到其它的函数里面,你也可以在函数里返回函数。使用函数作为参数的函数,或者返回函数的函数,这些函数被称为高阶函数(higher-order functions)。

例子:

const robot = (name, action) => {
  return action(name)
}

const greet = (name) => {
  return `hello, ${name}`
}

const greeting = robot(' 🐶 ', greet)
// 返回 “ hello, 🐶  ”

robot 是个函数,它支持两个参数,nameaction,这里的 action 参数的类型是一个函数。在 robot 里面返回了 action ,并且把 name 参数的值交给了 action。接下面我们又定义了一个函数叫 greet,它接受一个参数是 name ,这个函数会返回一个字符串。

然后我们用了一下 robot 这个函数,设置了一下它的两个参数的值,name 参数的值是 ' 🐶 'action 参数的值是我们定义的 greet 这个函数。执行的结果就会是:

hello, 🐶

我们可以再去定义一下函数:

const goodbye = (name) => {
  return `bye, ${name}`
}

然后再用一下 robot 函数:

const byeBye = robot(' 🐙 ', goodbye)

这次会返回:

bye, 🐙

完整的例子:

const robot = (name, action) => {
  return action(name)
}

const greet = (name) => {
  return `hello, ${name}`
}

const greeting = robot(' 🐶 ', greet)
console.log(greeting)

const goodbye = (name) => {
  return `bye, ${name}`
}

const byeBye = robot(' 🐙 ', goodbye)
console.log(byeBye)

结语

前前后后查了很多资料,看了很多实例和教程,因为是一个月前的笔记,当时找到的参考资料已经忘了,所以这次就无参考,大概就这样子

内容比较多,至此~

Responses