学习目标
熟悉函数的概念,能够说出函数的作用
掌握函数的定义与调用,能够根据程序需要定义函数并且完成函数的调用
掌握函数参数的设置,能够根据程序的需要设置相关参数
掌握如何获取函数调用时传递的所有实参,能够通过arguments对象获取实参
熟悉函数内外变量的作用域,能够区分全局变量和局部变量
掌握函数表达式,能够实现函数表达式的定义与调用
掌握匿名函数,能够实现匿名函数的定义与调用
掌握回调函数,能够实现回调函数的定义与调用
掌握函数嵌套与作用域链,能够定义与调用嵌套函数并能够描述作用域链的概念
掌握递归函数,能够实现递归函数的定义与调用
熟悉什么是闭包函数,能够说出闭包函数的用途
掌握闭包函数,能够实现闭包函数的定义与调用
在日常开发中,若程序中有多个重复的功能,例如数组排序,如果每次用到该功能时都编写一遍该功能的逻辑代码,非常麻烦,而且当需要修改该功能的逻辑代码时,需要多处修改,为此,JavaScript提供了函数。函数可以避免相同功能代码的重复编写,将程序中重复的代码封装起来,提高程序的可读性,减少开发者的工作量,便于后期的维护。本章将针对函数的内容进行详细讲解。
函数用于封装一段完成特定功能的代码,相当于将包含一条或多条语句的代码块“包裹”起来,用户在使用时只需关心参数和返回值,就能完成特定的功能。函数的优势在于提高代码的复用性,降低程序维护的难度。
在开发一个功能复杂的模块时,可能需要重复编写大量代码,这时可以使用自定义函数将重复的代码封装起来,在需要时直接调用即可。
自定义函数的语法格式如下。
function 函数名([参数1, 参数2, …]) {
函数体
}
函数的定义由以下4部分组成:
function:定义函数的关键字。
函数名:可由字母、数字、下画线和$符号组成,不能以数字开头,不能是JavaScript中的关键字。
参数:外界传递给函数的值,此时为形参,可选的,多个参数之间使用“,”分隔。
函数体:由函数内所有代码组成的整体,专门用于实现特定功能。函数体内使用return关键字可以返回函数的结果。
通过“函数名()”的方式即可实现函数的调用,小括号中可以传入参数,其语法格式如下。
函数名称([参数1, 参数2, …])
参数表示实参,“[参数1,参数2…]”表示实参列表,实参个数可以是零个、一个或多个。通常情况下,函数的实参列表与形参列表顺序对应,当函数体内不需要参数时,调用时可以不传参。
函数定义与调用的编写顺序不分前后。
函数在定义时根据参数的不同,可分为两种类型。 无参函数:定义函数时不设置参数的函数。 有参函数:定义函数时设置了参数的函数。
无参函数适用于不需要提供任何数据即可完成指定功能的情况,示例代码如下。
function greet() {
console.log('Hello everybody!');
}
在自定义函数时,即使函数的功能实现不需要设置参数,小括号“()”也不能省略。
在项目开发中,若函数体内的操作需要使用用户传递的数据,此时函数定义时需要设置形参,用于接收用户调用函数时传递的实参,示例代码如下。
function maxNum(a, b) {
a = parseInt(a);
b = parseInt(b);
return a >= b ? a : b;
}
(1)含有默认值的参数 在设置函数的形参时,还可以为其指定默认值。当调用者未传递该参数时,函数将使用默认值进行操作,示例代码如下。
(2)剩余参数 在函数定义时,除了可以指定具体数量的形参外,还可以利用“…变量名”的方式动态的接收用户传递的不确定数量的实参,示例代码如下。
function transferParam(num1, ...theNums) {
console.log(theNums); // 在控制台输出用户调用函数时传递的剩余参数
}
transferParam(0,1, 2, 3, 4);
若定义transferParam()函数时,所有参数的数量都不确定,则可以修改成以下形式。
function transferParam(...theNums) {
console.log(theNums); // 在控制台输出用户调用函数时传递的参数
}
transferParam(1, 2, 3, 4);
若不能确定函数的形参个数,定义函数时可以不设置形参,在函数体中直接通过arguments对象获取函数调用时传递的实参。 arguments是当前函数的一个内置对象,所有函数都内置了一个arguments对象,该对象保存了函数调用时传递的所有实参。实参的个数可通过arguments.length获取,具体的实参值可通过数组遍历的方式进行获取。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
function sum() {
console.log(arguments.length); // 输出实参个数
console.log(arguments); // 输出arguments对象
var result = 0;
for (i = 0; i < arguments.length; i++) {
result = result + arguments[i];
}
console.log(result);
}
sum(1, 2, 3, 4, 5);
</script>
</body>
</html>
案例需求:本案例将实现求任意两数的最大值。
实现思路:
定义函数,接收两个将要进行比较的数字。
利用选择结构语句实现比较两个数字的大小,返回最大值。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
function maxNum(num1, num2) {
if (num1 > num2) {
return num1;
} else {
return num2;
}
}
console.log(maxNum(10, 30));
</script>
</body>
</html>
通过前面的学习,我们知道变量需要先定义后使用,但这并不意味着,定义变量后就可以在任意位置使用该变量。
在自定义函数中定义一个age变量,在函数外进行访问输出,示例代码如下。
function info() {
var age = 18;
}
info();
console.log(age); // 报错,提示Uncaught ReferenceError: age is not defined
“age is not defined”,表示age变量没有被定义。之所以报错是因为age变量只能在info()函数体内使用。
变量需要在被定义时的区域内才可以使用,这个区域是变量的作用范围,被称为变量的作用域。JavaScript根据作用域使用范围的不同,可以将变量划分为以下两种变量。
全局变量:不在任何函数内定义(显式定义)的变量或在函数内省略var定义(隐式定义)的变量,它的作用域称为全局作用域,在同一个页面文件中的所有脚本内都可以使用。
局部变量:在函数体内利用var关键字声明的变量称为局部变量,它的作用域称为函数作用域,仅在该函数体内有效。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
var a = 'one'; // 全局变量
function test() {
var a = 'two'; // 局部变量
console.log(a);
}
test();
console.log(a);
</script>
</body>
</html>
通过前面的学习,相信大家已经掌握了函数的使用。但函数的内容不仅仅包括函数的定义与调用、函数参数的设置和函数内外变量的作用域等,还包括函数表达式、匿名函数以及回调函数等。
函数表达式指的是将函数赋值给变量的表达式,通过“变量名()”的方式即可完成函数的调用,小括号“()”内可以传入参数,示例代码如下。
var fn = function sum(num1, num2) { // 定义求和函数表达式
return num1 + num2;
};
fn(2, 3); // 调用函数
函数表达式的定义必须在调用前,且函数调用时采用的是“变量名()”的方式。
函数定义的方式不限制定义与调用的顺序。
函数表达式中的函数名如果不需要可以省略。
团队合作完成项目时,程序员经常定义一些函数来实现特定的功能,在给这些函数命名时经常会遇到与其他人取相同名字的情况,如何来解决命名冲突问题呢?
使用JavaScript中的匿名函数可以有效避免函数名的冲突问题。所谓匿名函数指的是没有名字的函数,也就是在定义函数时省略函数名。
匿名函数可以通过函数定义的方式实现调用,下面介绍匿名函数的3个使用场景。
函数表达式中省略函数名
匿名函数自调用
处理事件
利用函数表达式实现匿名函数,调用时使用“变量名()”,示例代码如下。
var fn = function (num1, num2) {
return num1 + num2;
};
通常情况下,如果函数的返回值需要使用变量来接收时,可以使用函数表达式来实现匿名函数的调用,并且可以通过“变量名()”的方式调用多次。
使用小括号“()”直接包裹匿名函数,示例代码如下。
(function (num1, num2) {
console.log(num1 + num2);
})(2, 3);
匿名函数后小括号“()”表示给匿名函数传递参数并立即执行,完成函数的自调用。自调用只能调用一次。
使用匿名函数处理单击事件,示例代码如下。
document.body.onclick = function () {
alert('Hi, everybody!');
};
多学一招:箭头函数
箭头函数是ES6中新增的函数,它用于简化函数定义的语法,其语法格式如下。() => { };
箭头函数的小括号中可以传入参数,调用箭头函数时可以将箭头函数赋给一个变量,然后通过变量名实现箭头函数的调用。
箭头函数的示例代码如下。
var fn = (num1, num2) => {
return num1 + num2;
};
箭头函数存在以下两种特殊情况:
(1)省略大括号和return关键字
在箭头函数中,当函数体只有一句代码,且代码的执行结果就是函数的返回值时,可以省略函数体的大括号以及return关键字。
var fn = (num1, num2) => num1 + num2;
(2)省略参数外部小括号
在箭头函数中,当参数只有1个时,可以省略参数外部的小括号。
var fn = name => {
console.log(name);
};
项目开发中,若想要函数体中某部分功能由调用者决定,则可以使用回调函数。
回调函数是由开发者预先定义好的一个函数,通常会作为参数传递给被调用的函数。当被调用的函数执行时,会在特定的时机调用开发者传入的回调函数。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
function cooking(flavour) {
// 厨师做菜
var food = '鱼香肉丝';
food = flavour(food);
return food;
}
var food = cooking(function (food) {
return food += '特辣';
});
console.log(food);
</script>
</body>
</html>
在开发项目时,一个复杂的功能往往需要定义多个函数来完成,对于其中一个函数而言,它可能依赖另外一些函数才能运行。但是,如果我们希望这些依赖的函数只能在本函数内部访问,其他函数不能访问,这时候可以把这些依赖的函数定义在本函数内部,这样在一个函数内部定义了其他函数就形成了嵌套函数。
对于嵌套函数而言,内层函数只能在外层函数作用域内执行,在内层函数执行的过程中,若需要引入某个变量,首先会在当前作用域中寻找,若未找到,则继续向上一层级的作用域中寻找,直到全局作用域,我们将这种链式的查询关系称为作用域链。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
var i = 26;
function fn1() { // 定义的第1个函数
var i = 24;
function fn2() { // 定义的第2个函数
function fn3() { // 定义的第3个函数
console.log(i);
}
fn3();
}
fn2();
}
fn1();
</script>
</body>
</html>
递归调用是函数嵌套调用中一种特殊的调用,它指的是一个函数在其函数体内调用自身的过程,这种函数称为递归函数。递归调用可以利用简单的代码实现复杂的计算。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
function factorial(n) { // 定义回调函数
if (n == 1) {
return 1; // 递归出口
}
return n * factorial(n - 1);
}
var n = prompt('求n的阶乘\n n是大于等于1的正整数,如2表示求2!。');
n = parseInt(n);
if (isNaN(n)) {
console.log('输入的n值不合法');
} else {
console.log(n + '的阶乘为:' + factorial(n));
}
</script>
</body>
</html>
斐波那契数列又称黄金分割数列,指的是“1, 1, 2, 3, 5, 8, 13, 21……”这样一个数列,从中可以找出的规律是“这个数列从第3项开始,每一项都等于前两项之和”。本案例将利用递归实现计算斐波那契数列第N项的值。
案例的实现思路:
如果N小于0则提示输入的数字不能小于0。
如果N等于0返回第0项的值为0。
如果N等于1返回第1项的值为1。
如果N大于1则进行递归调用,实现前两项值相加。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
function recursion(num) {
if (num < 0) {
return '输入的数字不能小于0';
} else if (num == 0) {
return 0;
} else if (num == 1) {
return 1;
} else if (num > 1) {
return recursion(num - 1) + recursion(num - 2);
}
}
console.log(recursion(10));
</script>
</body>
</html>
在JavaScript中,内层函数可以访问定义在外层函数中的所有变量和函数,并包括其外层函数能访问的所有变量和函数。但是在函数外部则不能访问函数的内部变量和嵌套函数。
在项目开发中,当我们需要在函数外部访问函数内部变量和嵌套函数时可以利用闭包函数来访问。
所谓闭包函数,指的就是有权访问另一函数作用域内变量(局部变量)的函数,它主要的用途是以下两点:
在函数外部读取函数内部的变量。
让变量的值始终保持在内存中,直到页面关闭。
因为闭包会使得函数中的变量的值一直被保存在内存中,内存消耗很大,所以闭包的滥用可能会降低程序的处理速度,造成内存消耗等问题。
闭包函数常见的实现方式是在一个函数A内部创建另一个函数B,然后将函数B作为函数A的返回值返回。当调用函数A后,调用者就会得到函数B,通过函数B来访问函数A中的局部变量。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
function fn1() {
var num = 0;
function fn2() {
++num;
console.log('我被调用的第' + num + '次');
}
return fn2;
}
var fn = fn1();
fn();
fn();
fn();
</script>
</body>
</html>
在日常生活中,每个公司的考勤组一般会先计算出每个月员工应出勤的天数,这样方便考勤人员记录考勤。而2月份是一个特殊的月份,考勤人员在计算2月份应出勤天数时非常麻烦,因为2月份的天数由年份决定,年份分又为平年和闰年,平年的2月份只有28天,闰年的2月份有29天。本案例将实现获取指定年份的2月份的天数。
案例的实现思路:
定义函数,用于返回指定年份的2月份的天数,在函数中接收用户输入的年份。
定义函数,用于判断用户输入的年份是否为闰年。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
// 用户输入年份,输出当前年份2月份的天数
function feb() {
var year = prompt('请输入年份:');
if (leapYear (year)) {
alert('当前年份是闰年,2月份有29天');
} else {
alert('当前年份是平年,2月份有28天');
}
}
feb();
// 判断是否为闰年的函数
function leapYear(year1) {
var flag = false;
if (year1 % 4 == 0 && year1 % 100 != 0 || year1 % 400 == 0) {
flag = true;
}
return flag;
}
</script>
</body>
</html>
本章首先讲解了初识函数,主要讲解了函数的概念、函数的定义与调用,以及函数参数的一些使用细节,并结合求任意两数的最大值的案例帮助读者掌握函数的使用;然后讲解了函数内外变量的作用域、匿名函数、嵌套与递归和闭包函数;最后通过一个动手实践的案例,帮助读者更好地理解函数。通过本章的学习,希望读者能够熟练掌握函数的使用。