01-函数与闭包
# 函数的基本使用
函数就是语句的封装,可以让这些代码方便地被复用。函数具有”一次定义,多次调用” 的优点。使用函数,可以简化代码,让代码更具有可读性。
和变量类似,函数必须先定义然后才能使用。使用 function
关键字定义函数,function 是“功能”的意思。
# 函数的定义和调用
函数的定义
匿名函数
执行函数体中的所有语句,就称为“调用函数”。调用函数非常简单,只需在函数名字后书写圆括号对即可。
fun() // 调用函数
# 函数声明提升
和变量声明提升类似,函数声明也可以被提升。
注意:如果函数是用函数表达式的写法定义的,是不能提升的。
为什么这种写法不能提升呢?
因为用 var 本质上不是定义函数,而是在定义变量,把函数赋值给这个变量而已,而变量的提升是之提升定义,不提升值的。
即提升的只是 fun 的定义,没有提升 fun 的值,fun 的值此时是 undefined,而 undefined 不能加上括号运行的,只有函数才能运行。
函数优先提升
**JS 会先提升函数定义,再提升变量定义。**通过下图详细介绍。
JS 会先提升所有的函数定义,之后才会提升所有的变量定义。变量声明提升只提升定义不提升值,所以 var fun
不会覆盖已经提升的函数 function fun
。
第一次调用 fun()
弹出 B 是因为函数提升了,并且不会被变量覆盖已经被提升的函数,所以执行的是标准定义 function fun
第二次调用 fun()
弹出 A 是因为在程序执行阶段匿名函数会赋值给 fun
,而执行阶段不会去再去解析函数体了,所以 function fun
就忽略了,fun()
调用执行的是被赋值的匿名函数。
# 函数的参数
参数是函数内的一些待定值,在调用函数时,必须传入这些参数的具体值。函数的参数可多可少,也可以没有。
形参和实参:
**JS 中形参和实参个数不要求一定相等。**实参多了,多余的实参没有形参接收,函数体读取不到;实参少了,没有接收到实参的形参值为 undefined。
# 实参列表 arguments
函数内 arguments
表示它接收到的实参列表,它是一个类数组对象(实际并不是数组)。
类数组对象:所有属性均为从 0 开始的自然数序列,并且有 length 属性,和数组类似可以用方括号书写下标访问对象的某个属性值,但是不能调用数组的方法。
arguments 对象是最常见的类数组对象,它是实参列表。
可以使用 arguments
来获取实参列表的长度和每个实参值。
function fun() {
console.log('实参个数为:' + arguments.length)
for (int i = 0; i < arguments.length; i++) {
console.log('第' + i + '个实参:' + arguments[i])
}
}
2
3
4
5
6
# 函数的返回值
函数体内可以使用 return
关键字表示“函数的返回值”
# 函数作用域
# 全局变量和局部变量
JavaScript 是函数级作用域编程语言:变量只在其定义时所在的 function 内部有意义。
如果不将变量定义在任何函数的内部,此时这个变量就是全局变量,它在任何函数内都可以被访问和更改。
如果函数中也定义了和全局同名的变量,则函数内的变量会将全局的变量“遮蔽”。
# 函数中的变量提升
注意考虑变量声明提升的情况
# 作用域链
先来认识函数的嵌套:一个函数内部也可以定义一个函数。和局部变量类似,定义在一个函数内部的函数是局部函数。
在函数嵌套中,变量会从内到外逐层寻找它的定义。
# 不加var将定义全局变量
在初次给变量赋值时,如果没有加 var,则将定义全局变量。
function fun() {
a = 3;
}
fun();
console.log(a); // 3
2
3
4
5
注意这里在 fun 函数外使用变量 a 之前必须要先调用一次 fun(),然函数体执行,才会定义处全局变量。
# 闭包
# 从一个题目看闭包
先从一个题目来看起,这段 JS 的执行结果是什么。
function fun() {
var name = '前端';
function innerFun() {
alert(name);
}
return innerFun();
}
var inn = fun();
inn(); // 执行结果是什么?
2
3
4
5
6
7
8
9
10
11
分析:
- 题目中定义了一个函数 fun,函数 fun 内部有一个局部变量 name 和一个局部函数 innerFun
- 调用外部函数 fun 就能得到内部函数,用变量 inn 来接收
- 执行 inn 函数,就相当于在 fun 函数的外部执行了内部函数
答案:执行结果是弹出“前端”,即使内部函数 innerFun 放到外部执行任然能读到内部的变量,即使在外部定义一个全局变量 name,读到的还是 fun 内部的变量 name。
这种特性就是闭包。
# 什么是闭包
JavaScript 中的函数会产生闭包(closure)。闭包是函数本身和该函数声明时所处的环境状态的组合。(闭包 = 函数 + 环境)
函数能够“记忆住”其定义时所处的环境,即使函数不在其定义的环境中被调用,也能访问定义时所处环境的变量。
根据这个特性,上题的现象就得以解释了。对于内部 innerFun 函数来说也有闭包,保护函数 innerFun 本身和它所处的环境,环境中包含局部变量 name,即使 innerFun 放在外部执行依然能访问定义时的环境。
在 JavaScript 中,每次创建函数时都会创建闭包。但是,闭包特性往往需要将函数“换一个地方”执行,才能被观察出来。
# 闭包的功能
闭包很有用,因为它允许我们将数据与操作该数据的函数关联起来。这与“面向对象编程”有少许相似之处。
闭包的功能:① 记忆性;②模拟私有变量。
下面通过题目来介绍这两个功能。
闭包用途一:记忆性
当闭包产生时,函数所处环境的状态会始终保持在内存中,不会在外层函数调用后被自动清除。这就是闭包的记忆性。
题目:创建体温检测函数 checkTemp(n),可以检查体温 n 是否正常,函数会返回布尔值。但是疫情下,不同的场所有不同的体温检测标准,比如小区体温合格线是37.3℃,而医院体温合格线是37.0℃,应该怎么编程呢?
分析:这里程序设计的痛点是 checkTemp(n) 只接收一个参数,不能传入场所来进行区分。可以使用闭包的记忆性来解决。
// 用于创建 checkTemp 函数的函数
function createCheckTemp(standardTemp) {
// checkTemp 函数会产生闭包,闭包会记忆局部变量 standardTemp
function checkTemp(n) {
if (n <= standardTemp) {
alert('体温正常');
} else {
alert('体温高');
}
}
}
// 创建一个 checkTemp 函数,以 37.0 度为标准线
var checkTemp_hospital = createCheckTemp(37.0);
// 创建一个 checkTemp 函数,以 37.3 度为标准线
var checkTemp_neighborhood = createCheckTemp(37.3)
checkTemp_hospital(37.2) // 弹出体温高
checkTemp_neighborhood(37.2) // 弹出体温正常
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
闭包用途二:模拟私有变量
在 Java、C++ 等语言中,有私有属性的概念,但是 JavaScript 中只能用闭包来模拟。
题目:请定义一个变量 a,要求是能保证这个 a 只能被进行指定操作(如加 1、乘 2),而不能进行其他操作,应该怎么编程呢?
// 封装一个函数,这个函数的功能就是私有化变量
function fun() {
var a = 0; // 局部变量
// 返回一个对象
return {
get: function() { return a; },
add: function() { a++; },
pow: function() { a *= 2; }
}
}
var obj = fun();
console.log(obj.get())
obj.add()
obj.pow()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
该例子中,通过 fun 得到的对象中并没有私有变量这个概念,fun 中有一个局部变量 a,可以通过闭包模拟,将 a 当作私有变量。
# 闭包使用的注意点
不能滥用闭包,否则会造成网页的性能问题,严重时可能导致内存泄露。所谓内存泄漏是指程序中己动态分配的内存由于某种原因未释放或无法释放。
下面看一个闭包的面试题:
function addCount() {
var count = 0;
return function() {
count = count + 1;
console.log(count);
}
}
var fun1 = addCount();
var fun2 = addCount();
fun1(); // 1
fun2(); // 1
fun2(); // 2
fun1(); // 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
调用了两次 addCount() 得到 fun1 和 fun2,在内存中会分别产生独立的闭包,之间互不影响。所以调用 fun1 和 fun2 操作的是不同的 count。
# 立即执行函数
# IIFE 概念和写法
IIFE(Immediately Invoked Function Expression,立即调用函数表达式)是一种特殊的 JavaScript 函数写法,一旦被定义,就立即被调用。
形成 IIFE 的方法:
函数不能直接加圆括号被调用。
函数必须转为 “函数表达式” 才能被调用。
# IIFE 的作用
作用一:为变量赋值
为变量赋值:当给变量赋值需要一些较为复杂的计算时(如 if 语句),使用 IIFE 显得语法更紧凑。
var age = 12;
var sex = '男';
var title = (function () {
if (age < 18) {
return '小朋友'
} else {
return sex == '男' ? '先生' : '女士';
}
})();
2
3
4
5
6
7
8
9
作用二:将全局变量变为局部变量
先看一个题目:因为 i
是全局变量,数组中所有函数都共享,所以 arr[2]()
弹出 5。
我们的预期是 arr[2]()
弹出 2,可以用 IIFE 实现。
IIFE 可以在一些场合(如 for 循环中)将全局变量变为局部变量,语法显得紧凑。
分析:
- 这里使用 IIFE,第 7 行 IIFE 调用处的 i3 为实际参数,分别等于循环变量 0、1、2、3、4。
- 由第三行的形参 i2 和循环变量 i1 没有关系,用于接收第 7 行传过来的实参
- 所以全局变量 i1 变成了局部变量 i2,对于函数来说 i2 参与组成了闭包
所以数组中 5 个函数都读的是闭包中的局部变量。