终于抽出时间去做简单的整理,内容比较基础,主要是颗粒化了知识点,方便日后查找加深理解,算是笔记吧
函数式编程
简单说,"函数式编程"是一种"编程范式"(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
,在这个被返回的函数里,会返回最终的结果。这里返回的就是把 greeting
与 name
参数的值组织成了一个新的字符串。
这种函数用起来像这样:
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
是个函数,它支持两个参数,name
与 action
,这里的 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)
结语
前前后后查了很多资料,看了很多实例和教程,因为是一个月前的笔记,当时找到的参考资料已经忘了,所以这次就无参考,大概就这样子
内容比较多,至此~
本文由 Chakhsu Lau 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。