在我工作时,总有一些总结性的想法,但是我并不擅长总结,只爱说大白话,直到我开始接触函数式编程,我才明白,这就是我一直想表达的写代码方式,哈哈,真是吃了没文化的亏。
我们常听说的编程范式有面向过程编程、面向对象编程,以及函数式编程。
- 面向过程编程: 字面意思,就是小时候写的流水账的日记一搬,我今天要吃早餐,就要先起床,下地,煎鸡蛋,等等,初入编程的我们基本都是这样的。
- 面向对象编程:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系。到了公司多写几个需求,多封装几个小组件,便有了这一层感悟。
- 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
x -> f(联系、映射) -> y,y=f(x)
函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x),x和y的关系
相同的输入始终要得到相同的输出(纯函数)
函数式编程用来描述数据(函数)之间的映射
上面这几行天花乱坠的一直在表达这个意思:大白话:函数式编程就是你高中数学的学的那个函数的意思,当输入确定了,输出也会确定。
(定义虽绕,但是他是十分严谨科学的,要多读)
比如来看这么一个简单例子,实现一个两个数相加
// 非函数式
let num1 = 2
let num2 = 3
let sum = num1 + num2
console.log(sum)
// 函数式
function add (n1, n2) {
return n1 + n2
}
let sum = add(2, 3)
console.log(sum)
函数式就会有一个函数,叫add,当输入的n1,n2确定时,是不是得到结果一定是确定的!这是多么的可控!而且又是多么的能加以复用!完全安全无副作用!给我整激动了。
再往下读的话,需要三点前置知识:
1.函数是一等公民
2.高阶函数
3.闭包
函数是一等公民
一等公民这个词可以大概看下这个 函数是一等公民,总结来说,在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。比如说字符串就是一等公民。
在 js 中函数就是一个普通的对象 (可以通过 new Function() ),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的入参数和返回值。对吧,看起来挺废话的,不要着急。
高阶函数
高阶函数 (Higher-order function) 就是HOF,用react的同学看到这个是不是有点熟悉,对,react的高阶组件叫HOC,其本质就是个高阶函数,那么啥是高阶函数呢
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回结果
函数作为参数
你手动实现一个forEach就明白了,forEach的功能是循环数组,并对每一项做你想做的操作,来,上代码
// forEach
function forEach (array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
多简单,这里的fn就是函数作为参数,我们想想这样有什么好处,它是不是让我们的forEach方法更灵活,而且在调用的时候,也不需要考虑它内部是如何实现的,爱用Lodash的小伙伴看到这请开始有一些感悟。
函数作为返回值
前端会经常遇到这样一个场景,用户连续多次点击了提交按钮,但是我们希望绑定的那个submit函数只执行一次,这就需要我们来实现一个once函数,让传进去的函数即使被多次调用,确只执行一次,上代码。
function once(fn) {
let flag = false; // 设定一个标识位,当fn执行后变成true
return function () {
if(!flag) {
flag = true;
return fn.apply(this, arguments)
}
}
}
// 这里来定义这个提交的函数
let submit = once(function (num) {
console.log(`提交的数字是${num}`)
});
// 试一下
submit(10) // 只有这个会输出
submit(20)
submit(30)
这个例子可能开始有点绕了,return来return去的,请务必自己手动实现一下,屡清思路,多写几遍就好了,这里的关键就是第三行那,return回一个函数,这个函数来判断是否需要执行入参的那个函数。
高阶函数的意义
抽象可以帮我们屏蔽细节,只需要关注我们想要的功能,高阶函数是用来抽象通用的问题,再看一遍这个例子。
// 面向过程的方式
let array = [1, 2, 3, 4]
for (let i = 0; i < array.length; i++) {
console.log(array[i])
}
// 高阶函数
let array = [1, 2, 3, 4]
forEach(array, item => {
console.log(item)
})
你看,这就是对循环的抽象,不需要关心循环具体实现的细节了,代码又简洁,又灵活。
常用高阶函数
- forEach
- map
- filter
- every
- some
- find ...
//我们再挑一个some实现一下
function some(array, fn) {
let result = false;
for(let i = 0 ; i < array.length; i++) {
if(fn(array[i])){
result = true;
break
}
}
return result
}
通过上面的forEach和some,他们都可以接收一个函数,这就是高阶函数,通过把一个函数传给另一个函数,可以让这个函数更灵活
闭包
闭包:可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
老生常谈的问题了,它只是定义比较绕,其实概念并不复杂
我们经常会在不经意间就使用它,比如我们上面那个once的例子
function once(fn) {
let flag = false;
return function () { // 如果once执行,返回的是这个函数,我们叫它f1
if(!flag) {
flag = true;
return fn.apply(this, arguments)
}
}
}
// 这里来定义这个提交的函数
let submit = once(function (num) {
console.log(`提交的数字是${num}`)
});
// 这里once执行后,submit=f1,once从执行栈中释放,
// 但是f1这个函数依然需要从它的外部的once函数中的取得flag变量,所以这里的flag变量不会被释放
// 试一下
submit(10) // 只有这个会输出
submit(20)
闭包的本质就是:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
我们来实现一个闭包的案例
现在要写一个统计员工工资的需求,假设这里是阿里,工资由基本工资+绩效,员工分p5,p6等职级,每个职级的基本工资不一样, 同职级本月绩效也不一样
// 多数人很可能就写成了这样
function getSalary(base, jixiao) { // base是基本工资
return base + jixiao;
}
let zhangsan = getSalary(10000, 3000); // 假设张三是p5,p5基本工资10000
let lisi = getSalary(20000, 2000); // 假设李四是p6,p6基本工资20000
这样的话,每次都还要输入各个职级的基本工资
// 使用闭包
function makeSalary(base) {
return function (jixiao) {
return base + jixiao
}
}
let getSalaryP5 = makeSalary(10000);
let getSalaryP6 = makeSalary(20000);
let zhangsan = getSalaryP5(3000);
let lisi = getSalaryP6(2000)
用控制台查看闭包,使用上面的例子
断点到执行makeSalary前,可以看到
CallStack(函数的调用栈):此时是anonymous,是个匿名函数,因为这外面就有一层script,他就相当于一个匿名函数在执行
Scope(作用域):可以看到当前Script的作用域中,有各种变量,由于js有变量提升,现在都是undefined;Global是全局作用域,指向window
按F11,向下走进makeSalary里
CallStack:栈顶出现了makeSalary函数
Scope:Local,当前作用域,出现base=10000,this指向window
继续到makeSalary执行完毕
CallStack:makeSalary执行完出栈了
Scope:刚才的Local作用域也移除掉了,Script中 getSalaryP5得到了执行结果,是个函数
代码继续走走走,走到执行getSalaryP5里去
CallStack:这个匿名函数进来了
Scope:重点看这个多出来的Closure,它就是闭包,(makeSalary)代表外部函数,里面的base就是相关的变量,所以正如之前所说,外部函数执行完就会移除,但是跟闭包相关的变量会被缓存下来
继续走到执行getSalaryP6里去
一目了然
整理这玩意真是太费劲了,删了改,改了删的,下节整理纯函数及柯里化
函数式编程(二)