# JavaScript教程 - 11 函数进阶
# 11.1 高阶函数
什么是高阶函数?
满足以下两种情况之一的函数就是高阶函数:
- 函数作为参数传入
- 函数作为返回值返回
高阶函数有什么作用呢?
高阶函数能让你写出更简洁、可复用、模块化的代码,它是回调函数、事件处理、函数组合等场景的核心。
下面慢慢讲。
# 1 函数作为参数
例如,我们现在有一个学生列表:
class Student {
constructor(name, chinese, math, english) {
this.name = name;
this.chinese = chinese;
this.math = math;
this.english = english;
}
}
// 学生列表
const allStuList = [
new Student("zhangsan", 87, 48, 92),
new Student("lisi", 47, 92, 71),
new Student("wangwu", 58, 46, 38),
new Student("zhangliu", 95, 91, 99),
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
现在分别想查询列表中:语文成绩不及格的同学、所有课程不及格的同学、所有课程都是优秀的学生,那么我们需要定义三个函数,如下:
// 学生列表
const allStuList = [
new Student("zhangsan", 87, 48, 92),
new Student("lisi", 47, 92, 71),
new Student("wangwu", 58, 46, 38),
new Student("zhangliu", 95, 91, 99),
];
// 获取语文成绩不及格的同学
function getChineseFlunkList() {
const stuList = [];
for (let stu of allStuList) {
if (stu.chinese < 60) {
stuList.push(stu);
}
}
return stuList;
}
// 获取所有课程不及格的同学
function getFlunkList() {
const stuList = [];
for (let stu of allStuList) {
if (stu.chinese < 60 && stu.math < 60 && stu.english < 60) {
stuList.push(stu);
}
}
return stuList;
}
// 获取所有课程都是优秀的学生
function getExcellentList() {
const stuList = [];
for (let stu of allStuList) {
if (stu.chinese >= 90 && stu.math >= 90 && stu.english >= 90) {
stuList.push(stu);
}
}
return stuList;
}
// -------------方法调用---------------------
console.log("语文不及格的同学:");
const chineseFlunkList = getChineseFlunkList();
for (let stu of chineseFlunkList) {
console.log(stu);
}
console.log("所有不及格的同学:");
const flunkList = getFlunkList();
for (let stu of flunkList) {
console.log(stu);
}
console.log("所有成绩都是优秀的同学:");
const excellentList = getExcellentList();
for (let stu of excellentList) {
console.log(stu);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
- 在上面的代码中,定义了3个函数
getChineseFlunkList()
、getFlunkList()
、getExcellentList()
,但是三个函数中只有其中的判断条件不同,代码比较冗余, 能否抽出判断条件,将判断条件作为参数传递呢?
我们先抽出一个公共的函数,根据传递的参数条件获取学生列表的函数:
// 根据条件获取学生列表
function filter(funCondition) {
const stuList = [];
for (let stu of allStuList) {
if (funCondition(stu)) {
stuList.push(stu);
}
}
return stuList;
}
2
3
4
5
6
7
8
9
10
- 上面的函数的参数也是一个函数,在函数中,调用传递的函数,根据函数的结果判断
stu
是否满足条件。 - 这样就可以传递一个函数,具体判断的条件,在传递的函数中实现就可以了,返回
true
就是满足条件。
下面来调用上面的函数:
// 使用示例
console.log("语文不及格的同学:");
const chineseFlunkList = filter((stu) => {return stu.chinese < 60});
for (let stu of chineseFlunkList) {
console.log(stu);
}
2
3
4
5
6
调用函数,并传递一个函数作为参数,为了简单,直接传递一个箭头函数就可以了。
stu.chinese < 60
表示如果学生的中文成绩大于60就返回true,那么在filter()
函数中调用传递的函数,就可以根据结果筛选学生了。
箭头函数还可以再简化一下,获取三个学习列表,可以这样写:
// 使用示例
console.log("语文不及格的同学:");
const chineseFlunkList = filter((stu) => stu.chinese < 60);
for (let stu of chineseFlunkList) {
console.log(stu);
}
console.log("所有不及格的同学:");
const flunkList = filter(
(stu) => stu.chinese < 60 && stu.math < 60 && stu.english < 60
);
for (let stu of flunkList) {
console.log(stu);
}
console.log("所有成绩都是优秀的同学:");
const excellentList = filter(
(stu) => stu.chinese >= 90 && stu.math >= 90 && stu.english >= 90
);
for (let stu of excellentList) {
console.log(stu);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
通过函数式编程,代码的冗余度降低了,代码更简洁了。
这种方式更符合编程的 OCP (Open-Closed Principle)原则,对扩展开放,对修改关闭。如果有其他的查询学生的条件,直接扩展即可,不用修改 filter()
函数。
# 2 函数作为返回值
函数作为返回值也经常被用到。
举个栗子:
你开了一家咖啡店,需要根据顾客的不同身份生成问候语:
// 高阶函数 - 返回定制化的问候函数
function createGreeter(role) {
return function (name) {
if (role === "VIP") {
return `尊贵的${name}会员,欢迎光临!`;
} else if (role === "regular") {
return `${name},您好!今天想喝点什么?`;
} else {
return `欢迎${name}~`;
}
};
}
// 生成特定角色的问候函数
const greetVIP = createGreeter("VIP");
const greetRegular = createGreeter("regular");
// 使用示例
console.log(greetVIP("张先生")); // "尊贵的张先生会员,欢迎光临!"
console.log(greetRegular("李女士")); // "李女士,您好!今天想喝点什么?"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 在上面的代码中,首先调用
createGreeter()
函数,函数也返回一个函数。 - 通过调用返回的函数,就可以实现不同的结果了。
函数作为返回值,经常是创建闭包最常见的方式,待会讲一下闭包。
# 11.2 内置高阶函数
JavaScript 中的内置高阶函数主要是 数组方法,它们都支持 传入函数作为参数。下面介绍一下常用的高阶函数,可以参考适用场景进行使用。
常用内置高阶函数:
方法 | 作用 | 是否返回新数组 | 是否常用 |
---|---|---|---|
forEach() | 遍历数组,执行副作用 | 否 | ✅ |
map() | 映射数组,返回新数组 | ✅ | ✅✅ |
filter() | 过滤数组,返回符合条件的项 | ✅ | ✅✅ |
reduce() | 聚合数组成单个值 | 否(返回值) | ✅✅✅ |
find() | 查找第一个符合条件的元素 | 否(返回元素) | ✅ |
some() | 至少一个符合条件返回 true | 否 | ✅ |
every() | 所有都符合条件返回 true | 否 | ✅ |
sort() | 排序,接收比较函数 | 原地修改 | ✅ |
flatMap() | 类似 map 后 flat 一层 | ✅ | ⚠️ |
# 1 forEach
作用:一般就是用来遍历数组的。
前面遍历数组的时候已经用到了,举个栗子:
const fruits = ["apple", "banana", "cherry"];
fruits.forEach((item, index) => {
console.log(`${index + 1}. ${item}`);
});
// 执行结果:
1. apple
2. banana
3. cherry
2
3
4
5
6
7
8
9
# 2 map
作用:对每一项进行转换处理,返回一个新数组(不会改变原数组)。
举个栗子,将数组中的数据乘以2倍,返回一个新的数组:
const nums = [1, 2, 3];
const newArray = nums.map((item) => item * 2);
console.log(newArray); // [2, 4, 6]
2
3
- 给 map 传递一个函数,函数的参数
item
是每个元素,可以对每个元素进行处理,回调函数的返回值就是新数组的元素。 - 适用于对数组中的数据进行转换;
- 回调函数还有两个参数,第二个是index,第三个是被遍历的数组本身。
# 3 filter
作用:筛选符合条件的元素,返回新数组。
举个栗子,筛选数组中的偶数,组成一个新的数组。
const nums = [1, 2, 3, 4, 5];
const evens = nums.filter(item => item % 2 === 0); // 判断item是否能被2整除
console.log(evens); // [2, 4]
2
3
- 回调函数的返回值为 true,就表示将该元素添加到新数组中;
- 可以用来根据条件筛选元素,和前面我们编写的
filter
函数类似。 - 回调函数还有两个参数,第二个是index,第三个是被遍历的数组本身。
# 4 reduce
作用:将数组归纳为一个值(例如求和等)。
举个栗子:求和
const nums = [1, 2, 3, 4];
const sum = nums.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 10
2
3
- 在上面的代码中,
reduce
函数有两个参数,第一个是回调函数,第二个是初始值,也就是第一次调用回调函数时候的acc
的值。acc
表示累计的值,cur
表示当前正在处理的元素。 - 上面是将数组的各个元素求和。
其实回调函数有四个值,第三个为当前元素的索引,第四个为调用 reduce
函数的数组本身。
再举个栗子,将数组中的元素变为一个对象的属性:
const arr = ['a', 'b', 'c'];
const obj = arr.reduce((acc, cur, i) => {
acc[cur] = i;
return acc;
}, {});
console.log(obj); // { a:0, b:1, c:2 }
2
3
4
5
6
- 在上面的代码中,初始值就是一个空对象
{}
,每次调用就给这个空对象添加属性和赋值acc[cur] = i
。 - 注意,需要返回归纳的结果值
acc
。
reduce
函数适合将数组变为单个值。
# 5 find
作用:返回第一个符合条件的元素(不是数组)。
举个栗子:
const users = [
{ id: 1, name: "Tom" },
{ id: 2, name: "Jerry" }
];
const jerry = users.find(user => user.name === "Jerry");
console.log(jerry); // { id: 2, name: "Jerry" }
2
3
4
5
6
- 就是查找数组中满足条件的第一个元素。
# 6 some
作用:判断是否至少有一个元素符合条件,返回布尔值。
举个栗子:
const nums = [1, 3, 5];
let hasEle = nums.some((x) => x % 2 === 0); // 判断数组中是否有偶数
console.log(hasEle); // false
2
3
- 用来判断数组中是否有一个元素满足条件。
# 7 every
作用:判断是否所有元素都符合条件。
举个栗子:
const nums = [2, 4, 6];
console.log(nums.every(x => x % 2 === 0)); // true,判断数组中是否都是偶数
2
# 8 sort
作用:对数组排序,将修改原数组。
讲解数组的时候,也举过例子了:
let arr = [10, 2, 30];
arr.sort(); // 排序
console.log(arr); // [10, 2, 30] → ["10", "2", "30"] → 按字符排序
arr.sort((a, b) => a - b); // 排序,可以传递一个函数,按照数字比较两个元素的大小,进行排序,实现升序或者降序
console.log(arr);
2
3
4
5
6
- 注意:如果不传递参数,默认是按字符排序。
- 可以传递参数,实现按照元素大小排序,返回
a-b
结果是升序,返回b-a
结果是降序。 - 注意:会修改原始数组!
# 9 flat
作用:对数组进行扁平化处理,减少数组的嵌套层级。
前面讲解数组的时候,就是数组中放元素,但是元素其实还可以是数组,也就是进行嵌套,而 flat()
函数就是对数组进行扁平化处理。
举个栗子:
const arr1 = [1, 2, [3, 4]];
console.log(arr1.flat()); // 对数组进行扁平化处理,默认只会扁平化一层
// [1, 2, 3, 4] (默认 depth=1)
const arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat());
// [1, 2, 3, 4, [5, 6]] (只扁平化一层)
console.log(arr2.flat(2));
// [1, 2, 3, 4, 5, 6] (扁平化两层)
2
3
4
5
6
7
8
9
10
- 对数组进行扁平化处理,如果不指定参数,默认只会扁平化一层。
# 10 flatMap
作用:先使用 map 映射,然后再 flat 一层。
举个栗子:
const sentences = ["Hello world", "Goodbye universe"];
const words = sentences.flatMap((sentence) => sentence.split(" "));
console.log(words); // ["Hello", "world", "Goodbye", "universe"]
2
3
- 在上面的代码中,
sentence.split(" ")
表示将每个元素,使用空格" "
进行分割,那么数组变为:[["Hello", "world"], ["Goodbye", "universe"]]
,每个元素被被变为数组了,那么数组就变为两层了。 - 然后在使用
flat()
函数进行扁平化处理。
# 11.3 闭包
# 1 闭包简介
如果要实现一个计数器的功能,每调用一次函数,打印的数值加1,那么该如何实现呢?
let count = 0;
function counter() {
count++;
console.log(count);
}
counter(); // 1
counter(); // 2
counter(); // 3
2
3
4
5
6
7
8
9
你可能像上面这样实现,但是这样实现会有一个问题,计数的变量是全局变量,谁都可以修改,那么就会出问题。
修改一下代码,如下:
function outer() {
let count = 0; // 外部函数的变量
function inner() {
count++; // 内部访问外部函数变量
console.log(count);
}
return inner; // 返回内部函数
}
let counter = outer(); // 调用外部函数,返回内部函数
counter(); // 1
counter(); // 2
counter(); // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 既然全局变量大家都能修改,那么就把变量放在函数中,内部函数引用了外部函数的变量,所以这个变量在全局是无法访问和修改的;
- 然后将内部函数作为外部函数的返回值,进行返回;
上面的实现就形成了一个闭包,所以说**什么是闭包?**闭包必须满足三个条件:
- 在一个外部函数中有一个内部函数;
- 内部函数必须引用外部函数中的变量;
- 外部函数的返回值必须是内部函数;
当我们想要隐藏一些数据,不希望被别人访问时,就可以使用闭包。
# 2 闭包的本质
先看一下下面的代码:
let count = 2;
function func1() {
console.log(count);
}
function func2() {
let count = 3; // 外部函数的变量
func1();
}
func1(); // 2
func2(); // 2
2
3
4
5
6
7
8
9
10
11
12
13
14
- 在上面的代码中,调用
func1()
和func2()
都打印 2,也就是说在func1()
函数中访问的 count 变量就是全局的 count,不会因为在不同的调用调用结果就不同。
也就是 JavaScript 中的作用域是词法作用域,在函数定义的时候就决定了能访问哪些变量。
# 3 闭包注意事项
在上面使用闭包的时候,每调用一次外部的函数就会产生一个闭包。而闭包会使函数的外部变量始终保存内存中,不会被作为垃圾回收,容易造成内存泄漏。
闭包中的变量只要它们“能被访问”,就不会被回收;一旦所有对闭包的引用都消失了,闭包和它的变量也就会被垃圾回收器清理。
# 11.4 函数的递归
什么是函数的递归?
函数的递归,就是在一个函数内部,又调用了这个函数自己,这个就是函数的递归。
举个例子:
function funA() {
console.log("----");
funA();
}
funA();
2
3
4
5
6
上面的函数,执行 funA()
,然后在 funA()
内部又调用了自己,那么会重新又调用了 funA()
函数,然后又调用了自己,这样就会一直无限调用,变成了无限循环调用,执行的时候很快就报Stack Overflow的错误,栈溢出。
所以函数的递归有时候是很危险的,很容易无限调用,造成栈溢出,程序崩溃。所以函数的递归调用一定要注意 结束或跳出递归 的条件。
例如我们写一个用递归求阶乘的函数:
/**
* 求阶乘
*/
function factorial(num) {
if (num <= 1) {
return 1;
}
return num * factorial(num - 1);
}
let num = 5;
let result = factorial(num);
console.log(num + "的阶乘为:" + result); // 120
2
3
4
5
6
7
8
9
10
11
12
13
14
以 5 举例,求 5 的阶乘,调用了 factorial()
函数 ,则计算 5 乘以 4 的阶乘,然后求 4 的阶乘,重新调用了 factorial()
函数,然后计算 4 乘以 3 的阶乘,依次类推,一直得到1的阶乘,然后向上返回。
递归函数一定得有结束的条件,否则就会无限递归导致栈溢出错误。
这里暂时理解不了,可以不纠结,慢慢领悟,平时也很少用到。
# 11.5 可变参数
“可变参数”是指函数能够接收 不定数量的参数。可以根据需要传入任意数量的参数值。
可变参数主要有两种方式:
- 传统方式:
arguments
对象; - 现代推荐方式:
...rest
参数。
# 1 arguments对象
arguments
是一个 类数组对象(和数组的操作很像,但是不是数组),包含了传入函数的所有参数;- 它只在非箭头函数中有效;
- 它是函数中的隐藏参数,没有显式声明,但可用,名称是固定的,就是arguments。
举个栗子:
function sum() {
let sum = 0;
for (let i = 0; i < arguments.length; i++) { // 可以通过arguments获取到传入的所有参数
sum += arguments[i]; // 可以通过数组一样访问到传入的参数
}
return sum;
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4)); // 10
2
3
4
5
6
7
8
9
10
需要注意:
- arguments 不是数组,所以无法直接使用如
map
,filter
,forEach
等方法; - 但是可以通过
Array.from(arguments)
或[...arguments]
转换为数组;
arguments
对象总是接收到函数的所有参数,不管函数有没有其他的参数:
function sum(a, b) {
console.log(a); // 1
console.log(b); // 2
console.log(arguments); // [1, 2, 3, 4]
}
console.log(sum(1, 2, 3, 4)); // 10
2
3
4
5
6
7
# 2 ...方式
而 ...rest
参数方式的参数就真的是一个数组了。
举个栗子:
function sum(...numbers) {
return numbers.reduce((acc, val) => acc + val, 0); // 使用 reduce() 方法进行求和
}
console.log(sum(1, 2)); // 输出 3
console.log(sum(1, 2, 3, 4)); // 输出 10
2
3
4
5
6
- 上面的
numbers
就真的是一个数组,可以使用数组的方法; - 参数名称是可以自定义的;
...rest
可变参数参数可以和固定参数一起使用,但是需要注意,可变参数最多只能有一个,而且必须作为最后一个参数。
function sum(a, b, ...numbers) {
return a + b + numbers.reduce((acc, val) => acc + val, 0); // 使用 reduce() 方法进行求和
}
console.log(sum(1, 2)); // 输出 3
console.log(sum(1, 2, 3, 4)); // 输出 10
2
3
4
5
6
- 在上面的代码中,优先传递参数给前面的固定参数,剩余参数才会传递给可变参数。
为什么更推荐使用 ...rest
参数?
arguments
不是数组,...rest
是数组 ;arguments
不支持箭头函数,...rest
支持 ;arguments
在严格模式下行为有陷阱;举个栗子,在非严格模式下,
arguments
和命名参数是绑定的:function test(a) { arguments[0] = 99; // 传递的a的值为1,这里修改了第一个参数的值为99 console.log(a); // a的值也发生了变化,为99 } test(1);
1
2
3
4
5在严格模式下不再绑定:
"use strict"; function test(a) { arguments[0] = 99; // 修改arguments的值 console.log(a); // 输出 1(未变) } test(1)
1
2
3
4
5
6
7这种不一致性会导致潜在 bug,不如
...rest
安全可靠。
# 11.6 call和apply
函数除了可以使用 ()
进行调用,还可以使用 call()
和 apply()
方法进行调用。
举个栗子:
function test() {
console.log('Hello')
}
test.call();
2
3
4
5
- 对象会有属性和方法,而函数也是对象,而
call()
和apply()
其实是函数对象的方法。
上面这样调用函数,有点脱裤子放屁的感觉!
前面在讲函数中的 this
指向谁,基本可以归纳为以下几种:
- 以函数方式调用,this指向window对象;
- 以对象方式调用,指向当前调用方法的对象;
- 构造函数中的this,指向的是当前创建的对象;
- 箭头函数中没有自己的this,this 指向的是外层作用域。
其实 call()
和 apply()
的主要作用是改变函数内部的 this
指向。
举个栗子:
function test(a, b, c) {
console.log(this, a, b, c)
}
let person = {name:'Doubi'}
test.call(person, 1, 2, 3); // {name: 'Doubi'} 1 2 3
2
3
4
5
6
call
的第一个参数,会成为函数中的 this,后面的参数会成为函数的参数。
apply()
函数也是一样的,只是 call()
和 apply()
传递参数的方式不同。call()
函数传递参数的方式是一个一个传递,apply()
函数是通过数组传递。
举个栗子:
function test(a, b, c) {
console.log(this, a, b, c)
}
let person = {name:'Doubi'}
test.call(person, 1, 2, 3); // {name: 'Doubi'} 1 2 3
test.apply(person, [1, 2, 3]); // {name: 'Doubi'} 1 2 3
2
3
4
5
6
7
# 11.7 bind
bind()
方法的作用,也是可以改变函数中 this
的指向,但是和 call
和 apply
的不同, 它不会立即执行函数,而是返回一个新的函数,将这个新函数中的 this 绑定。
举个栗子:
function test(a, b, c) {
console.log(this, a, b, c)
}
let person = {name:'Doubi'}
let newFunc = test.bind(person, 1, 2);
newFunc(4, 5, 6);
2
3
4
5
6
7
8
test.bind(person, 1, 2)
会返回一个新的函数,函数中的this
指向bind()
方法的第一个参数,也就是person
。bind()
方法后面的参数是对参数进行绑定,也就是这个新函数的第一个参数被绑定为1,第二个参数被绑定为2了,调用新函数时传的参数将无效。
执行结果:
{name: 'Doubi'} 1 2 4
- 新函数的前两个参数被绑定为1和2 了,调用的时候传递的参数无效。
所以 bind() 可以创建一个新函数,为新函数绑定 this 和参数。
需要注意,箭头函数中没有this,所以不能使用 call、apply、bind 修改它的 this 指向。
← 10-数组 12-对象和数组补充 →