函数的分类与定义函数的方式
JavaScript中的函数可以分为两类:有名函数
与匿名函数
。而定义函数的方式有两种:函数声明
与函数表达式
。
目标:定义一个函数 fn ==> 有名函数1
2
3
4
5
6
7
8// 使用函数声明
function fn(){
// 函数执行体
}
// 使用函数表达式
var fn = function(){
// 函数执行体
}
使用函数声明的重要特征就是函数声明提升
,即在读取代码前会先读取函数声明。函数名()
表示执行函数
看看下面的代码没有任何问题。1
2
3
4
5
6
7
8
9
10
11
12
13// 定义一个有名函数 fn1 使用函数声明
function fn(){
console.log("fn1")
}
// 调用函数 fn1
fn1(); // fn1
// 定义一个有名函数 fn2 使用函数表达式
var fn2 = function(){
console.log("fn2")
}
// 调用函数 fn2
fn2(); // fn2
但是如果是把调用放在定义函数前面,使用函数表达式
的就会报错(Uncaught ReferenceError: fn1 is not defined)1
2
3
4
5
6
7
8
9
10
11
12
13// 调用函数 fn1
fn1(); // fn1
// 定义一个有名函数 fn1 使用函数声明
function fn(){
console.log("fn1")
}
// 调用函数 fn2
fn2(); // Uncaught ReferenceError: fn1 is not defined
// 定义一个有名函数 fn2 使用函数表达式
var fn2 = function(){
console.log("fn2")
}
这就是使用两种的区别。
函数的返回值
每一个函数在调用的时候都会默认返回一个undefined
。1
2
3
4
5
6function fn(){
console.log(1)
}
fn(); // 1
console.log(fn); // console出一个函数 即 fn
console.log(fn()); // undefined
这里需要注意的地方就是关于函数执行过程
与函数执行结果
。
fn()
表示调用函数。那就会执行函数体。并默认返回一个undefined
。只不过这个值undefined
没有变量接收或者说是我们没有用这个值。
console.log(fn)
就只是console出一个变量fn
的值。只不过这个值是一个函数。
console.log(fn())
与第一个的区别就是函数执行了并返回了一个结果。这个结果呢与上面不同的就是现在这个结果我们用上了(放在了console)里面。再由于值是undefined
,所以console了一个undefined
。
既然函数是可以有返回值的,并且这个值默认是一个undefined
。那我们可以可以修改呢?答案是可以的。
我们使用return
语句可以让函数返回一个值1
2
3
4
5
6
7function fn(){
console.log(1)
return "哈哈"
}
fn(); // 1
console.log(fn); // 函数 fn
console.log(fn()); // 哈哈
可以看一下第一个与第二个的结果与之前的是相同的。但是第三个的结果就不同了,他是字符串哈哈
。因为我们修改了函数的默认返回值。
所以,可以把默认函数理解成这样的1
2
3function fn(){
return undefined;
}
而我们修改返回值就是吧这个undefined给变成其他的值了。注意:
函数的返回值可以是任意的数据类型。
函数参数
函数是可以接收参数的,在定义函数的时候放的参数叫形式参数
,简称形参
。在调用函数的时候传递的参数叫实际参数
,简称实参
。一个函数可以拥有任意个参数1
2
3
4
5
6
7
8
9
10
11function fn(a,b){
console.log(a)
console.log(b)
console.log(a+b)
}
// 调用函数并传递参数
fn(3,5); // 3,5,8
fn(3); // 3,undefined,NaN
fn(3,5,10) // 3,5,8
可以看看上面的例子。定义函数的时候有两个形参。调用的时候分为了三种情况。
第一种,传递两个参数,在console时候a=3,b=5,a+b=8
。老铁,没问题。
第二种,传递一个参数,在console的时候a=3,b=undefined,a+b=NaN
。哈哈,你不行。
第三种,传递3个。在console的时候a=3,b=5,a+b=8
。握草,你牛逼。对第三个参数视而不见了。
以上就是三种情况。一句话:参数一一对应,实参少了,那么没有对应的就是undefined,实参多了,多出来的就是没有用的
arguments
在不确定参数(或者定义函数的时候没有形参)的时候,调用函数你传递参数了,但是你没有使用新参去接收,就无法使用。把此时就有一个arguments
对象可以获取到实参的个数以及具体的值。1
2
3
4function fn(){
console.log(arguments)
}
fn(1,2,3,4,5,6,7) // Arguments(7) [1, 2, 3, 4, 5, 6, 7, callee: ƒ, Symbol(Symbol.iterator): ƒ]
arguments
在严格模式下无法使用。
函数递归
递归:就是函数自己调用自己。比如下面经典的阶层递归函数1
2
3
4
5
6
7
8function stratum(n){
if (n <= 1){
return 1;
} else {
return n * stratum(n - 1);
}
}
stratum(5) // 120 = 5 * (4 * (3 * (2 * 1) ) )
可以看出实现的阶层的功能。
不过需要注意一下每一个的执行顺序。不是5 * 4 * 3 * 2 * 1
。而是5 * (4 * (3 * (2 * 1) ) )
的顺序。为了证明这一点。可以将*
换为-
1
2
3
4
5
6
7
8function fn(n){
if (n <= 1){
return 1;
} else {
return n - fn(n - 1);
}
}
fn(5) // 3
如果是按照不带括号的5-4-3-2-1 = -5
。但是结果却是3
。那3
是怎么来的呢?5 - (4 - (3 - (2 - 1) ) ) = 5 - (4 - (3 - 1)) = 5 - (4 - 2) = 5 - 2 = 3
还可以使用arguments.callee
方法调用自身。这个方法就指向当前运行的函数1
2
3
4
5
6
7
8function stratum(n){
if (n <= 1){
return 1;
} else {
return n * arguments.callee(n - 1);
}
}
stratum(5) // 120
递归虽然可以让代码更简洁,但是能不使用递归的时候就不要使用,递归会影响性能(因为过多的调用自己会一直保存每一次的返回值与变量,导致内存占用过多甚至内存泄露)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18console.time(1);
function stratum(n){
if (n <= 1){
return 1;
} else {
return n * arguments.callee(n - 1);
}
}
console.log(stratum(5))
console.timeEnd(1) // 1: 4.470947265625ms
console.time(2)
var a = 1;
for (var i = 1; i <= 5; i++) {
a *= i;
}
console.log(a);
console.timeEnd(2) // 2: 0.2373046875ms
两个阶层,一看。for循环快太多了。具体的性能问题可以看看爱情小傻蛋关于递归的算法改进。
函数闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
两个条件:
- 函数嵌套函数
- 内部函数使用包含函数的变量或者是参数
1 | function fn(){ |
上面的例子中的函数就是一个闭包。注意上面的直接调用返回值与先保存返回值在调用的区别。
闭包只能取得包含函数中任何变量的最后一个值。this
是无法在闭包函数中调用的。因为每一个函数都有一个this
。
闭包函数中使用的变量是不会进行销毁的,像上面的var a = fn()
,这个函数a中使用了函数fn中的变量,且a
是一直存在的,所以函数fn
里面的变量a
是不会销毁的。如果是直接调用函数fn()()
只是相当于调用一次fn
函数的返回值。调用完函数返回值就销毁了。所以变量a
不会一直保存。
因为闭包函数的变量会一直保存不会
call,apply与bind
三个方法都是改变this指向
call apply
1 | function fn(a,b){ |
直接调用函数fn(1,2)
,this.name
的值是嘻嘻
如果使用call:1
fn.call(obj,1,2) // 1,2,哈哈
call
方法的第一个参数是改变this
指向的东西,可以是任何的数据类型。只不过如果是null
或者是undefined
就会指向window
。其他的参数依次对应函数的每一个形参。
如果使用apply
1
fn.apply(obj,[1,2])
apply
的使用与call的使用的唯一的区别就是它对应函数每一项形参是一个数组而不是单独的每一个。
call与applu都是在函数调用的时候去使用
bind
则是在函数定义的时候使用1
2
3
4
5
6
7
8
9
10
11
12function fn(a,b){
console.log(a)
console.log(b)
console.log(this.name)
}
var name = "嘻嘻"
var obj = {
"name": "哈哈"
}
// 执行函数fn
fn(1,2) // 1,2,嘻嘻
如果使用bind
可以是一下几种方式1
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// 使用函数表达式 + 匿名函数
var fn = function(a,b){
console.log(a)
console.log(b)
console.log(this.name)
}.bind(obj)
fn(1,2)
// 使用有名函数
function fn(a,b){
console.log(a)
console.log(b)
console.log(this.name)
}
fn.bind(obj)(1,2)
// 函数在自执行
(function fn(a,b){
console.log(a)
console.log(b)
console.log(this.name)
}.bind(obj)(1,2))
(function fn(){
console.log(a)
console.log(b)
console.log(this.name)
}.bind(obj))(1,2);
(function fn(){
console.log(a)
console.log(b)
console.log(this.name)
}).bind(obj)(1,2);
使用bind
的时候也是可以传递参数的,但是不要这样使用,因为使用bind
后你不调用函数那么参数还是没有作用。既然还是要调用函数,我们一般就把函数的实参传递到调用的括号里面。