7、函数
函数实际上是对象,每一个函数都是Function
类型的实例,而函数名就是指向函数对象的指针
简单地说,函数就是对象,它封装了一段可被重复调用执行的代码块,我们通过调用函数就可以实现大量代码的重复使用
7.1 定义函数
定义函数总共有四种方式:
- 函数声明
- 函数表达式
- 箭头函数(ES6)
Function
构造函数
1、函数声明
函数声明式我们最常用的定义函数的方法
function foo() {
console.log("Hello world!")
}
2、函数表达式
函数表达式几乎和函数声明式等价的
var foo = function() {
console.log("Hello world!")
}
该方式跟变量初始化一样
函数表达式有一点跟函数声明不同的是:它不会有变量提升的现象
3、Function
构造函数
let foo = new Function(console.log("Hello world!"));
这种定义函数的方法不推荐,因为这段代码会被执行两次:
- 第一次:把它当作常规代码来执行
- 第二次:解析传给构造函数的字符串
4、箭头函数
箭头函数以短小精悍著称,它和函数表达式有点像
let foo = (sum1, sum2) => { console.log(sum1 + sum2) }
ES6 新增的
7.2 函数的参数
函数的参数包括形参和实参
- 形参:定义函数时,定义给函数用的变量
- 实参:调用函数时,传给函数的数据
但是,js偏偏就与其它语言不同,js函数不关心你传入参数的个数,即使你在定义函数时规定要函数要接收两个形参,但是并不意味这你调用函数就得传入两个实参
你传入一个、两个、三个都行,甚至不传都行,解释器不会报错,也没有人说你;是不是惊掉下巴!
我们来看一下:
function foo(a, b) {
console.log(a + b);
};
foo(); // NaN
foo(1); // NaN
foo(1, 2, 3); // 3
这是为什么呢?
1、arguments
对象
arguments
何许人也?
arguments
是一个对象,而且是一个伪数组对象,伪数组不是Array
的实例
那它有什么作用呢?arguments
就是接收从外面传进来的实参的,传进来的实参按照先来后到的顺序依次存进去arguments
中,所以,第一个参数的键名是0,第二个是1,依次排序
一句话:arguments
是一个伪数组,负责存储传进来的参数
所以,我们在使用function
关键字定义的函数时,可以通过访问arguments
对象,来获取传进来的参数
一定得是
function
关键字定义的函数,不能是箭头函数,因为箭头函数不能使用arguments
我们来感受一下:
function foo(a, b) {
console.log(arguments[0] + arguments[1]);
}
foo(10, 20); // 30
我们还可以查看 arguments
中有多少个参数
function foo(a, b) {
console.log(typeof arguments);
console.log(arguments.length);
console.log(arguments);
}
foo(10, 20);
// object
// 2
// { '0': 10, '1': 20 }
现在可以解释为什么参数怎么传都不会报错
- 当我们少传或者不传的时候,
arguments
取出来的实际就是undefined
,undefined
本省相加或者与数字相加时,就会返回NaN
- 当我们多传时,js解释器只取它需要,其它不需要用到的放一边
所以,js函数的参数只是为了方便才写出来,这样不用很麻烦地写
arguments[0]
、arguments[1]
…,所以参数并不是必须写出来的
2、默认参数值
默认参数值就是我们可以给函数参数设置一个默认的数,当我们传参的时候,函数就是使用我们传递的参数,否则就是使用默认参数
在ES6之前,我们都需要自行检测是否有传参,没有就使用默认参数
// 当不传参的时候,b的类型就是undefined
function foo(a, b) {
b = (typeof b !== 'undefined') ? b : 20
console.log(a + b);
}
foo(10); // 30
在ES6之后支持显式定义默认参数
function foo(a, b=20) {
console.log(a + b);
}
foo(10); // 30
现在我们都是使用显式定义默认参数了
需要注意的是:使用默认参数时,
arguments
对象的值不反映参数的默认值,只反映传给函数的参数箭头函数同样也可以使用默认参数
7.3 函数的返回值
1、return
语句
我们函数只是实现功能,最终的结果需要返回给函数的调用者函数名( ),可以通过 return
实现
function share() {
return "只要你在 我会一直分享";
}
share(); // 执行函数(输出结果需要打印)
我们还可以把函数名share
赋给另外一个变量
var foo = share;
- 现在
foo
也是函数名了
注意:使用不带括号的函数名只会访问函数指针,而不会执行函数;此时
foo
和share
都是指向同一个函数
练习一下:
// 练习一
function getMax(a, b) {
if (a > b) {
return a
} else {
return b
}
}
console.log(getMax(10, 20)); // 20
// 练习二:使用三元表达式
function getMax1(a, b) {
return a > b ? a : b
}
console.log(getMax1(10, 20)); // 20
2、return
结束函数
return
函数也相当于一个终止语句,即return
语句之后的代码就都不会被执行了
function share() {
return '只要你在 我会一直分享!'
alert('一定会!'); // 并不会被执行
}
console.log(share()); // 只要你在 我会一直分享!
3、return
返回值
return
语句只能返回一个值
function getNum(a, b) {
return a, b;
}
console.log(getNum(10, 20)); // 10
4、函数如果没有return
函数如果没有return
,则它会返回undefined
function foo(a) {
a++;
};
console.log(foo(10)); // undefined
break
、continue
、return
的区别
函数 | 说明 | 主要针对对象 |
---|---|---|
break | 结束当前的循环体(如for 、while ) | 循环 |
continue | 跳出本次循环,继续执行下次循环(如for 、while ) | 循环 |
return | 不仅可以退出循环,还能返回return 语句中的值,同时还能结束当前的函数体内的程序 | 循环、函数 |
7.4 函数的调用
函数的调用有几种方式:
- 常规调用
- 被另外的函数调用
- 立即调用
1、常规调用
常规调用就是我们自己定义函数,然后自己调用
function share() {
return "只要你在 我会一直分享";
}
share(); // 调用函数
这种是再正常不过的了
2、被另外的函数调用
就是通过其他函数来调用自己
function foo1() {
console.log(10)
}
function foo() {
console.log(20)
foo1();
}
foo() // 一次性执行两个函数
但很多时候,函数是作为值被传入到另外的函数中
在js中,函数名就是就是变量,所以函数可以被使用在任何可以使用变量的地方。这就意味着不仅可以在一个函数中返回另一个函数,还可以把函数作为参数传给另一个函数
定义一个函数:
function add(a) {
return a + 20
}
把该函数作为参数传入另一个函数中:
// 第一个参数是传入函数,第二个是传入这个函数的数值
function callAdd(callback, num) {
return callback(num)
}
var result = callAdd(add, 10)
console.log(result); // 30
- 上面
callAdd
函数是为了访问callback
函数,而不是调用它,所以不能带括号 - 这种方式在回调函数中经常使用
3、立即执行函数
我们平时定义的函数,都是需要自己来调用的,但是有一种函数是自己执行的:立即执行函数
立即执行函数也叫立即调用函数表达式(IIFE
),以后看到 IIFE
就要知道是 立即执行函数
IIFE
由于被包含在括号中,所以会被解释为函数表达式
(function() {
函数体
})()
简单栗子:
(function() {
console.log('你好 世界!');
})(); // 你好 世界!
括号还可以传参数:
(function(a, b) {
console.log(a + b);
})(10, 20); // 30
立即执行函数在ES6以前可是有大作用的,那时尚未支持块级作用域,在var
大行其道的年代,使用var
关键字声明的循环迭代变量i
,它并不会被限制在for
循环中的块级作用域内
for (var i = 0; i < 5; i++) {}
console.log(i); // 5
外面居然访问得到块级作用域里面的变量,这是绝对不允许的,所以 IIFE
就出来“救场”了
(function() {
for (var i = 0; i < 5; i++) {}
})()
console.log(i); // 报错
将循环定义在 IIFE
中,就可以防止变量定义外泄;因为只要函数执行完毕,其作用链就会被销毁
但是,在ES6之后,let
声明关键字出世,块级作用域的变量就被锁定
for (let i = 0; i < 5; i++) {}
console.log(i); // 报错
- 所以, 现在就不必大动干戈使用
IIFE
了
7.5 私有变量
任何定义在函数中的变量,都是私有变量,外部是不能访问到的
function foo() {
let num = 10;
}
foo();
console.log(num); // 报错
- 无论使用
var
还是let
声明的变量都是私有变量
要想访问函数里面的变量,也不是没有办法,这就涉及到了另一个东西:闭包;这个我们后面再来讨论
7.6 递归函数
递归函数就是一个函数通过名称自己调用自己
我们计算 1 * 2 * 3 * 4 * 5
时,就可以使用递归思想:
function factorial(num) {
if (num === 1) {
return num
}
return num * factorial(num - 1);
};
let sum = factorial(5);
console.log(sum); // 120
其运算过程是这样的:
retrun 5 * factorial(4); // 第一次
retrun 5 * (4 * factorial(3)); // 第二次
retrun 5 * (4 * (3 * factorial(2))); // 第三次
retrun 5 * (4 * (3 * (2 * factorial(1)))); // 第四次
retrun 5 * (4 * (3 * (2 * (1)))); // 第五次
注意:当我们把函数赋给其它变量,再令factorial = null
,会出现报错
let anotherF = factorial;
factorial = null
console.log(anotherF(5)); // 报错
这是因为anotherF
保留了对函数的引用,调用它就需要递归调用 factorial
函数,但此时factorial
已不是函数了,所以会报错
我们可以创建一个命名函数表达式f()
,然后再它赋值给变量 factorial
,这样即使把函数赋值给其它变量,再怎么赋值就都不影响到新变量调用函数了,因为它递归调用的是f()
,f
不曾改变
let factorial = function f(num) {
if (num === 1) {
return num
}
return num * f(num - 1);
};
let anotherF = factorial;
factorial = null
console.log(anotherF(5)); // 120
再来看一道题:打印斐波那契数列
斐波那契数列:前两个数是1,后面的每个数都是前两个数之和(如:1, 1, 2, 3, 5, 8, 13…)
// 封装一个斐波那契数列的函数
let factorial = function f(num) {
if (num <= 2) {
return 1
}
return f(num - 1) + f(num - 2)
};
// 打印前10个数
var arr = []
for (var i = 1; i <= 10; i++) {
arr.push(factorial(i))
}
console.log(arr); // [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]