一、为什么函数是一等公民
1.1. 什么是一等公民
在编程语言中,当说到“一等公民”(First-class Citizen)这个术语时,它指的是语言中的基本抽象单位,它们被赋予了与其他基本数据类型(如数字、字符串、布尔值等)相同的地位和能力
1.2. 为什么称呼为“一等公民”
-
在1967年的论文《Fundamental Concepts in Programming Languages》中,斯特雷奇提出了
一等公民
的概念,用以描述在编程语言中具有特定地位的实体 -
其实一等这个词汇,就有一种优越性的表达含义,但在这里,其实是表示地位及重要性。简单的理解的话,就是编程语言之中一旦有内容被称为
一等公民
,该内容就是这门语言的核心内容
1.2.1. 实体定义
-
我们对
一等公民
的定义是:具备特定地位的实体。但这定义要怎么理解呢?毕竟就目前来看还是有点抽象-
可以被存储在数据结构中(如数组、列表、哈希表等)
-
可以参数传递
-
可以有返回值
-
可以被赋值给其他标识符
-
-
具备这些特性的内容,就被称为一等公民。在JavaScript中,是函数,而在Java中,则是对象
-
不同语言的一等公民所指的其实都不尽相同
-
但具备的特点是一样的
-
1.3. 一等公民的重要性
-
将函数视为一等公民的语言通常支持高阶函数和闭包,这为函数式编程范式提供了支持,而这也是我们本章节的主体,为了凸显其中的重要性,我们会在本章节中讲解高阶函数,进而在下一章节中深入闭包
-
一等公民的概念也强调了语言设计的灵活性和表达能力,允许我们以更抽象和模块化的方式编写代码,而这模块化思想也是目前主流的模式,在Vue和React等框架中,都有着强烈的运用风格
1.4. 定义之代码演示
-
可以参数传递,返回值,被赋值给其他标识符,我们要如何理解?
-
通过代码来学习是最快的方式之一,我们来看下JS中的函数是如何实现这些的
-
在函数中可以继续嵌套定义函数,其实是比较特殊的。其他语言,例如C或者Java都是不允许的
-
-
在该案例中,我们直接把函数返回出去的这种做法,其实是非常适合用在做工具库的情况,这有点像Class中的new实例化的效果。Class后续我们会学到的
-
无论是通过
foo()
获取的函数引用赋值给fn
,还是通过new
关键字实例化一个类获取的对象,最终都得到了一个可以调用或使用的东西
-
//作为另一个函数的参数,js语法允许函数内部再定义函数
function foo(){
function bar(){
console.log("小余的bar");
}
//返回值,我们直接把函数返回出去了,而非把函数结果返回出去。返回出去的是一个函数,还需调用
return bar
}
//赋值给其他标识符
var fn = foo()
fn()
//小余的bar
-
将函数直接作为参数传达进去的情况其实还是比较少见的
-
通常情况下,我们会使用一个变量来接收住bar的内容,再将变量传入foo函数内。但JS中确实是可以直接传达进去的
-
另一种情况则是传入函数本身,通常是为了实现某种特定的效果,借传入函数本身来实现
-
//也可以作为另外一个函数的返回值来使用
function foo (aaaa){
console.log(aaaa);
}
foo(123)
//也可以参数传递
function bar(bbbb){
return bbbb + "刚吃完午饭"
}
//将函数作为参数传达进去调用
foo(bar("小余"))
// 123
// 小余刚吃完午饭
1.4.1. 封装函数小案例
-
然后我们将其进行结合,看函数如何通过这三点定义进行表达。在这个案例中,我们主要使用calc函数
-
calc函数可以传达num1等参数(实参、形参)
-
add、sub、mul通过return实现返回值的定义效果
-
add、sub、mul被calc函数所使用,产生不同效果
-
-
该案例,通过对calc计算函数的高度抽象,实现了模块化的效果。只需要传入add、sub、mul等函数,即可实现所对应的效果
-
传入的三个函数,如同三个模块。需要则加载,不用则卸下
-
从这点也可以看出,函数是非常的灵活的
-
//封装小案例
function calc(num1,num2,calcFn){
console.log(calcFn(num1,num2));
}
function add(num1,num2){
return num1 + num2
}
function sub(num1,num2){
return num1 - num2
}
function mul(num1,num2){
return num1 * num2
}
calc(10,10,add)
calc(10,10,sub)
calc(10,10,mul)
//20
//0
//100
二、高阶函数
2.1. 什么是高阶函数
在JavaScript中,高阶函数是指至少满足以下一个条件
的函数:
-
接收一个或多个函数作为参数
-
返回一个函数
2.2. 挑选偶数
-
从高阶函数的定义来看,我们的封装函数小案例,满足第一点条件,也是属于一种高阶函数
-
我们通过正常使用和高阶函数使用,来分别实现原生JS的挑选偶数案例
-
2.2.1. 普通方式实现
-
我们一般的实现思路是在拥有JS基础知识情况下,正常的实现方式:
-
创建空数组用来接收偶数
-
遍历需要筛选偶数的数组
-
对每个数字进行判断是否为偶数
-
为偶数则填入创建的空数组
-
-
正常进行实现,其实是需要四个步骤的,所有的步骤都由我们手动的进行实现
//普通使用
var nums = [2,4,5,8,12,45,23]
var newNums = []
for(var i = 0;i<nums.length;i++){
var num = nums[i]
if(num % 2 === 0){
newNums.push(num)
}
}
console.log(newNums)
//[ 2, 4, 8, 12 ]
2.2.2. filter过滤器实现
-
filter过滤器是数组之中的方法
-
element: 当前正在处理的数组元素
-
index(可选): 当前元素的索引
-
array(可选): 调用
filter
的数组 -
语法:array.filter(callback(element[, index[, array]])[, thisArg])
-
callback: 函数,测试数组的每个元素。返回
true
表示保留元素,返回false
表示丢弃元素thisArg(可选): 执行
callback
时使用的this
值(暂时跳过,我们后面章节会讲this)
-
-
我们传入的其实是一个函数,而这个函数在内部会被调用。此时就会被称为回调函数callback,回调就是回头会调用的意思
-
所以实际上我们filter的参数只有两个,一个是回调函数,一个是绑定this。后者我们暂时不考虑
-
前者回调函数可接收三个参数
-
-
该高阶函数是过滤的意思,这说明了,我们可以直接对遍历的参数本身进行判断,产生的判断结果为布尔值,则会决定了当前数据是否返回,而不是返回布尔值本身。这就是该高阶函数
过滤
本身所具备的含义
//filter,对数组进行过滤,是数组中的一个方法,传入三个参数(第一个是数组中遍历的值,第二个是数组的对应下标,第三个是我们当前数组的引用=>就是整个数组传进来),返回值是另外一个新的数组
var nums = [2,4,5,8,12,45,23]
//方式1:明显的函数特征
var evenNumbers = numbers.filter(function(number) {
return number % 2 === 0; // 返回条件:元素是偶数
});
//方式2:箭头函数省略掉function
var newNums = nums.filter((item,index,array)=>{
return item % 2 === 0
})
console.log(newNums);
//[ 2, 4, 8, 12 ]
-
所以在了解我们的需求之后,只需要用到回调函数的第一个参数的情况下,我们连小括号都能省略掉
-
这个回调函数,我们nums数组内的每一个数字都会调用一次
-
同时可以注意到,我们没有了括号之后,不需要return。这是因为
{}
其实是函数的作用域,而一旦出现了作用域就会隔绝变量的提升,此时想要传递出去就需要return -
不使用
{}
的条件是,我们的逻辑非常简短,简短到就只有一行内容就可以使用 -
并且由于filter是使用在数组的方法,回调函数至少对一个元素返回
true
,否则返回一个空数组
-
var nums = [1, 2, 3, 4, 5, 6];
//方式3:省略掉小括号
var newNums = nums.filter(n => n % 2 === 0);
console.log(newNums); // 输出: [2, 4, 6]
2.2.3. map映射实现
-
map
映射:返回一个新数组,其元素是原数组元素经过指定函数处理后的结果 -
语法:array.map(callback(element[, index[, array]])[, thisArg])
-
callback
:回调函数,接收三个参数element
、index
和array
thisArg
:可选,指定执行回调函数时的this
值
-
-
我们能够看到,很多的高阶函数,都是传达进一个回调函数的
-
而回调函数有三四种写法,由复杂到简单,这在所有使用
回调函数
的高阶函数都是一样的 -
在目前的情况,我们这里直接用最简单的方式,3+4的情况。但遇到其他情况则可以自由组合
-
使用function,也被称为匿名函数或者匿名函数表达式,回调函数是最常见的场景,没有名称标识符
-
使用箭头函数
-
参数只有一个,箭头函数省略小括号
-
函数体简短到只有一行,可以省略
{}
和return
-
//高阶函数map映射的使用
//map:映射
var nums = [1, 2, 3, 4, 5, 6];
var newNums2 = nums.map(i => i % 2 === 0 ? '偶数是女生' : '基数是男生')
console.log(newNums2);
//[ '偶数是女生', '偶数是女生', '基数是男生', '偶数是女生', '偶数是女生', '基数是男生', '基数是男生' ]
2.2.4. forEach迭代实现
-
forEach
迭代:仅遍历数组的每个元素,无返回值,通常用于执行副作用操作 -
语法:array.forEach(callback(element[, index[, array]])[, thisArg])
-
参数和前面讲解的高阶函数一致
-
//forEech:迭代,没有返回值,通常就用来打印一些东西
var nums = [2,4,5,8,12,45,23]
nums.forEach(i => console.log(i))
// 2
// 4
// 5
// 8
// 12
// 45
// 23
2.2.5. find查找
-
find
查找:返回数组中第一个满足指定测试函数的元素,否则返回undefined
-
语法:array.find(callback(element[, index[, array]])[, thisArg])
-
参数一致,不重复
-
//find:查找的意思,有返回值
var nums = [2,4,5,8,"小余",12,45,23]
//找到内容则返回内容
var item = nums.find(i => i === '小余')
console.log(item);
//小余
//找不到则返回JS引擎处理的默认值undefined
var item2 = nums.find(i => i === 'coderwhy')
console.log(item2);
//undefined
-
通常用来查询一系列相关的内容也是非常好用的,这对于我们想要实现检索表格内容、个人信息等操作是常用的做法
//模拟数据
var friend = [
{name:"小余",age:18},
{name:"coderwhy",age:35},
]
const findFriend = friend.find(i => i.name === '小余')
console.log(findFriend);
//{ name: '小余', age: 18 }
//findIndex,找到对象在数组在对象中对应的索引值
const findFriend2 = friend.findIndex(i => i.name === '小余')
console.log(findFriend2);
//0
2.2.6. reduce累加
-
reduce
累加:累加数组元素,返回单一的结果值,通过对每个元素应用一个合并函数实现 -
语法:array.reduce(callback(accumulator, element[, index[, array]])[, initialValue])
-
callback
:回调函数,接收四个参数accumulator
(累加器)、element
(当前元素)、index
(当前索引)和array
(原数组)initialValue
:可选,累加器的初始值,如果不提供,则默认为数组的第一个元素
-
//普通实现方式
var nums = [2,4,5,8,12,45,23]
var total = 0
for(var i = 0;i<nums.length;i++){
total += nums[i]
}
console.log(total);
//99
//reduce:对我们原来的数组进行一些累加或者统计的操作
//高阶函数reduce的使用
//reduce接收参数,第一个参数:上一个函数的返回值(例如我们数组中有7个数字,那就调用7次函数,第一个参数每次都调用上一次的内容)
//那第一次调用的时候没有上一个函数怎么办?我们可以在回调函数后面定义初始化的值,例如0
//prevValue(上一次的值):0 , item:2 prevValue是previousValue的简写
//prevValue(上一次的值):2 , item:4
//不停的将上一次的值跟下一次的值做一个处理,直到全部处理结束带着结果进行返回
var num = nums.reduce((preValue, item) => preValue + item, 0)
console.log(num);
//99
2.3. 函数(Function)与方法(Method)的区别
-
一般来说,其实是指同一个东西
-
函数(Function)
:独立的Funtion,称之为一个函数 -
方法(Method)
:当我们的一个函数属于某一个对象时,我们称这个函数是这个对象的方法 -
方法更像是定义在一些特殊地方的函数,函数包含得更大
-
这就是为什么我们的高阶函数被称为方法的原因,因为都隶属于某个对象,所以我们才能直接通过
.xxx
进行调用 -
真想说的话,方法也可以叫做函数,但函数通常不会叫做方法
-
var obj = {
foo:function(){
}
}
//这个foo就是一个属于obj对象的方法
//方法调用的时候
obj.foo()
//函数调用,不属于某个对象
foo()
2.4. 高阶函数如何学习?
-
高阶函数的真正使用地方,应该是实际场景之中,单纯学习API的话,是不能够真正掌握的
-
我们目前只学习了语法,但具体能够运用在什么地方,是一个较为模糊的概念
-
而运用JS高阶函数最多的框架是React,刚好最近它已经要出19版本了,React是很优秀的前端框架,并且对于JS的掌握提升帮助非常之大,JS和React是相互印证的两面
-
学习JS为学习React打下坚实的根基
-
学习React为学习JS提供了绽放光芒的平台
-
-
所以,目前来看,在这里并不适合讲解这些高阶函数的运用场景。因为大家可以发现,过滤器能做的事情,映射也可以做。而映射能做的事情,迭代好像也可以做
-
那在具体的场景中,我们应该要用哪个呢?
-
我觉得这需要到具体的项目场景中去学习才不会虚无,我们目前学到这里就OK了,等到做项目的时候,我们才能体会其中带来的好处
-
等到ES6-ES14部分,我们也会学习更多的高级函数或者语法
-
2.4.1. 高阶函数的好处
-
在上面的五种高阶函数案例中,大家可以看到,我都是使用最精简的写法。这种精简的写法叫做箭头函数,在现在,我们只是为了凸起高阶函数的简洁而这么做的,但具体的学习箭头函数,我们会放在第接下来的第九篇文章当中,如果好奇的话,不妨先从MDN中进行了解
-
这样可以对比得更加明显,高阶函数的代码通常一行能解决正常方式的许多行
-
这是因为高阶函数的特性,能够接收函数或者返回函数。让高阶函数可以内置实现我们原本正常步骤中的其中几步,代码量的减少,其实是因为有一部分通用的步骤被高阶函数实现并且隐藏起来了,这些行为大多数都是从
最小特权原则
中引申出来的,也叫做最小授权或者最小暴露原则,在我们这样方法的API设计中是经常使用的软件设计模式 -
而将其隐藏起来,可以避免同名标识符之间的冲突,会更省心一些。毕竟这些内容,我们往往只需要通过它们处理好的数据
-
所以在一开始学习的时候,可能会稍微不好理解一点。但熟练之后,会为我们节约下不少时间
-
像上面的各种案例中,基本上都内置了
数据的遍历
这个操作,让我们的代码可以不用进行for循环,减少了最主要的代码工作量。而其他的高阶函数,所内置的虽不一定的数据遍历操作
-
-
但基本上都是某一部分通用的操作,目的是为了减少我们代码量和代码重复的痛点,而这也是高阶函数所想表达的意思
2.4.2. 记忆高阶函数
-
高阶函数有非常多,我们在学习的时候最好进行总结归纳
-
死记硬背是最不可取的方式
-
不管怎么样的函数,所进行的事情基本上无法脱离对数据的处理
-
而对数据的处理,基本上都来源于
增删改查
这四个字,依次套用就行了。对一段主体是处理开头还是结尾亦或者中间?现学套用
:内容增加在开头、增加在中间、增加在结尾、增加在具体索引位置其他的
删改查
都是一样的逻辑,单对数据处理位置的四种方式乘增删改查的四种情况。就有16种方法了,在稍微拓展一下其他有关于this的,然后对原数据有能改动、有返回新数据的两种情况,再乘2,差不多就四十多种方法了。所以通过简单的推导就能够去掌握大多数常用的部分 -
而且高阶函数的回调函数传递进参数,基本上都差不多
-
-
既然是对数据的处理,那还分为两种大类:
-
使用函数方法会改变原数据
-
...不会改变原数据
-
-
大多数情况下,我们会使用第一种方式,在React中,我们会学到类似
数据的不可变性
的思想,这有利于我们理解这些做法
2.5. 语法的学习
-
在看各种文档的时候,我们能够看到类似如下的语法示例:
array.find(callback(element[, index[, array]])[, thisArg])
-
好像很少有人说这种需要怎么去看,怎么去理解,而这些在我们进行自学的道路上,是不可避免需要接触到的内容
-
在这里核心注意点有两个,
方括号[]
以及逗号,
-
2.5.1. 括号
-
括号分为单层方括号
[]
和 嵌套方括号[[]]
单层方括号
[]
:可选参数:当我们看到用方括号括起来的参数,如
[element]
或[index]
,这表示这些参数是可选的。可以在调用函数时包括这些参数,也可以省略它们。方括号内的参数不是必须提供的,但提供这些参数可能会改变函数的行为或返回的结果嵌套方括号
[[]]
嵌套可选参数:嵌套的方括号,如
[[index]]
或[[array]]
,通常用来表示参数的依赖性。当外层的参数被包括时,内层的参数才有可能被包括。这种结构在定义中还是比较少见的,通常用来进一步强调参数之间的依赖关系-
我们需要如何区分他们呢?
-
-
而
element
是必需的,因为它没有被方括号括起来-
而不管是单层方括号还是嵌套方括号,都代表了可选的意思
-
2.5.2. 逗号
-
在文档中的这个语法,我们能够看到,逗号在方括号的里面...
-
这又是什么意思?为什么要在方括号的里面呢?
-
逗号则是必须要满足前面的内容,才能够轮到逗号之后的内容。是逗号前面是必须满足的前置条件
-
2.5.3. 语法拆解示例
-
在理解了逗号和方括号的含义之后,我们再来看下这个语法到底是什么意思
-
array.find(callback(element[, index[, array]])[, thisArg])
-
首先进行拆分
-
我们首先将其分为两个参数,第一个回调函数因为由
小括号()
,所以是必须的,不能够空参数。而第二个参数是方括号[]
,表示可选,并且在方括号内部参数的前面有逗号,说明了必须有第一个回调函数参数才能有第二个this剩余参数 -
为什么我知道得这么清楚?这是因为callback本身就是回调函数的意思,而thisArg则是this+Argument的含义,我们在之后会学到什么是argument的
-
回调函数内部则就是一样的套路了,回调函数里面必须有元素element才能有index索引,有索引才能有数组array本身
-
callback(element[占位符])
-
[,thisArg]
-
-
现在,让我们打开MDN,来看下这样的理解是否正确!
-
是正确的,但现在MDN没有采用这种精简的语法了,而是对参数进行解释,并分为必填和可选两个部分,以缩进来表示更加细分的参数,这会降低一些理解的难度
-
而在vscode的提示中,则是转为用TS来进行提示,本质上是差不多的,但TS所能表达的含义会更加丰富。等学完JS再去学TS,就对于各种现在的代码提示能够有很好的理解力了
-
图6-1 API语法参数分类