闭包
什么是闭包
闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
上面提到了局部变量,那什么又是局部变量呢? 在理解局部变量之前,我们需要先知道,一个函数的执行流程是什么样的。
执行上下文
执行上下文(execution context)是JavaScript中最重要的一个概念。执行上下文定义了变量或者函数有权访问的其他数据,决定了它们各自的行为。每个执行上下文都有一个与之关联的变量对象。
全局执行上下文是最外围的一个执行上下文。根据ECMAScript所在的宿主环境的不同,该上下文也不同。在浏览器中,全局执行上下文是windows对象,在全局环境下声明的变量为全局变量,所有的全局变量和函数都是作为window对象的属性和方法创建的。某个执行上下文中的所有代码执行完毕后,该上下文被销毁,保存在其中的所有变量和函数也随之销毁(全局执行上下文知道应用程序退出——例如关闭浏览器或者网页时才会被销毁)
而每个函数也都有自己的执行上下文,当执行流进入一个函数时,函数的环境就会被推入一个环境栈当中。而在函数执行完毕后,栈将其环境弹出,把控制权返回给之前的执行环境。
注意:执行上下文,顾名思义,是在某段代码执行时所定义的,所以它是动态的。某个函数在不同的环境调用时,可能会产生不同的执行上下文。
例如:
// 全局环境
var a = "全局环境";
function A() {
var a = "局部环境";
B();
}
function B() {
console.log(a);
}
A();
浏览器执行上述代码时的流程如下:
1、执行栈中推入全局执行上下文,全局执行上下文中存在全局变量a和函数A与函数B。
image.png2、执行流进入A函数,在执行栈顶推入函数A的执行上下文,函数A的执行上下文存在局部变量a。
image.png3、执行函数A代码块中的函数B调用指令,执行栈顶推入函数B的执行上下文,函数B打印变量a。
image.png此时,控制台打印出了全局变量。
根据执行栈的情况来说,理应是函数B逐层向下查找到函数A的执行上下文,并且打印出局部变量才对呀,为什么反而打印出了全局变量呢?
这里就引出了作用域这个概念
作用域
作用域:指的是一个变量的作用范围。
在ES6之前,JS的作用域只有全局作用域以及函数作用域两种,在ES6引入了块级作用域(本文暂且先不讨论块级作用域)。
一个变量如果是在全局环境下定义的,那么这个变量就存在于全局作用域下。以此类推,在函数内部定义的变量,则存在于函数作用域下。
作用域链
而JS的函数在声明时,会创建一个作用域链。它的用途是保证对执行上下文有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在的上下文的变量对象。如果这个上下文是函数,其变量对象为其内部声明的变量以及入参的arguments对象。作用域链中的下一个变量来自包含(外部)上下文,这样一直延续到全局执行上下文。而JS的函数在声明时,采用的是词法作用域,即在声明时就确定好了作用域链。作用域链的定义是静态的!
在上面的例子里,函数A和函数B在定义时,外部执行上下文只有全局执行上下文,所以其作用域链都为:
函数A/B作用域 -> 全局作用域。
所以,上文中的例子,函数B内部通过其作用域链先找其内部的变量对象,发现没有变量a,便向上通过作用域链找到了全局作用域下的全局变量,并最终打印出结果。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的方式,就是在一个函数内部返回一个匿名函数。我们来改造一下上面的那个例子:
var a = "全局环境";
function A() {
var a = "局部环境";
return {
B: function () {
console.log(a);
},
};
}
var obj = A();
obj.B(); // "局部环境"
现在,我们在函数A的内部返回了一个对象,该对象内部有一个函数B。我们在全局环境下调用了这个函数B,结果打印出了局部环境。这就是一个闭包,我们成功的在全局作用域下调用到了函数A作用域中的变量。究竟是怎么回事呢?让我们再来执行一下这段代码:
1、全局执行上下文推入执行栈中,该上下文中存在一个全局变量a,一个函数A和一个对象obj。
image.png2、调用函数A,执行栈中推入函数A执行上下文。该上下文中存在一个局部变量a。
image.png3、执行obj.B(),将函数B执行上下文推入执行栈中
image.png4、打印变量a,此时根据词法作用域,我们画出作用域链。函数A是在全局环境下声明的,所以其作用域链的下一部分指向了全局作用域,而函数B是在函数A中声明的,所以其作用域链的下一部分指向了函数A的函数作用域。
image.png此时,函数B的作用域链指向了函数A的作用域,因此打印出了局部变量。现在,我们通过闭包,可以在全局环境中使用函数作用域下的变量了。
抽象点来理解闭包:当一个函数内部返回了一个匿名函数,该匿名函数除了自身携带的物品外(内部的变量对象),还背着一个背包(通过作用域链引用的外部函数的变量对象)。在该函数外部就可以通过这个背包来访问这个函数内部的变量了
注意点:因为闭包返回出来的匿名函数有对其外部函数变量对象的引用,原本外部函数执行完后其变量对象等都会被内存回收,但由于闭包的存在,其外部函数的变量对象还在被引用,所以不会内存回收,容易造成内存泄漏。我们在使用闭包的同时,还要注意它所带来的副作用。
闭包案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript">
//面试经典问题:
function onMyLoad(){
/*
抛出问题:
此题的目的是想每次点击对应目标时弹出对应的数字下标 0~4,但实际是无论点击哪个目标都会弹出数字5
问题所在:
arr 中的每一项的 onclick 均为一个函数实例(Function 对象),这个函数实例也产生了一个闭包域,
这个闭包域引用了外部闭包域的变量,其 function scope 的 closure 对象有个名为 i 的引用,
外部闭包域的私有变量内容发生变化,内部闭包域得到的值自然会发生改变
*/
var arr = document.getElementsByTagName("p");
for(var i = 0; i < arr.length;i++){
arr[i].onclick = function(){
alert(i);
}
}
}
</script>
</head>
<body onload="onMyLoad()">
<p>产品一</p>
<p>产品二</p>
<p>产品三</p>
<p>产品四</p>
<p>产品五</p>
</body>
</html>
- 解决方法一:
/*
解决思路:
增加若干个对应的闭包域空间(这里采用的是匿名函数),专门用来存储原先需要引用的内容(下标),不过只限于基本类型(基本类型值传递,对象类型引用传递)
*/
for(var i = 0;i<arr.length;i++){
//声明一个匿名函数,若传进来的是基本类型则为值传递,故不会对实参产生影响,
//该函数对象有一个本地私有变量arg(形参) ,该函数的 function scope 的 closure 对象属性有两个引用,一个是 arr,一个是 i
//尽管引用 i 的值随外部改变 ,但本地私有变量(形参) arg 不会受影响,其值在一开始被调用的时候就决定了.
(function (arg) {
arr[i].onclick = function () { //onclick函数实例的 function scope 的 closure 对象属性有一个引用 arg,
alert(arg); //只要 外部空间的 arg 不变,这里的引用值当然不会改变
}
})(i); //立刻执行该匿名函数,传递下标 i(实参)
}
- 解决办法二
/*
解决思路:
将下标作为对象属性(name:"i",value:i的值)添加到每个数组项(p对象)中
*/
for(var i = 0;i<arr.length;i++){
//为当前数组项即当前 p 对象添加一个名为 i 的属性,值为循环体的 i 变量的值,
//此时当前 p 对象的 i 属性并不是对循环体的 i 变量的引用,而是一个独立p 对象的属性,属性值在声明的时候就确定了
//(基本类型的值都是存在栈中的,当有一个基本类型变量声明其等于另一个基本变量时,此时并不是两个基本类型变量都指向一个值,而是各自有各自的值,但值是相等的)
arr[i].i = i;
arr[i].onclick = function () {
alert(this.i);
}
}
- 解决办法三:
/*
解决思路:
与解决办法一有点相似但却有点不太相似.
相似点:同样是增加若干个对应的闭包域空间用来存储下标
不同点:解决办法一是在新增的匿名闭包空间内完成事件的绑定,而此例是将事件绑定在新增的匿名函数返回的函数上
此时绑定的函数中的 function scope 中的 closure 对象的 引用 arg 是指向将其返回的匿名函数的私有变量 arg
*/
for(var i = 0; i<arr.length;i++){
arr[i].onclick = (function(arg){
return function () {
alert(arg);
}
})(i);
}
- 解决办法四:
使用ES6新语法 let 关键字
<script type="application/javascript">
"use strict";//使用严格模式,否则报错 SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode
var arr = document.getElementsByTagName("p");
for(var i = 0;i<arr.length;i++){
let j = i;//创建一个块级变量
arr[i].onclick = function () {
alert(j);
}
}
</script>
3s后打印所有li元素的内容
let lis = document.querySelector('.nav').querySelectorAll('li')
for (let i = 0; i < lis.length; i++) {
(function (i) {
setTimeout(function () {
console.log(lis[i].innerHTML)
}, 3000)
})(i)
}
闭包详情
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
- 行1 - 8。我们在全局执行上下文中创建了一个新的变量createCounter,并赋值了一个的函数定义。
- 第9行。我们在全局执行上下文中声明了一个名为increment的新变量。
- 第9行。我们需要调用createCounter函数并将其返回值赋给increment变量。
- 行1 - 8。调用函数,创建新的本地执行上下文。
- 第2行。在本地执行上下文中,声明一个名为counter的新变量并赋值为 0;
- 行3 - 6。声明一个名为myFunction的新变量,变量在本地执行上下文中声明,变量的内容是为第4行和第5行所定义。
- 第7行。返回myFunction变量的内容,删除本地执行上下文。变量myFunction和counter不再存在。此时控制权回到了调用上下文。
- 第9行。在调用上下文(全局执行上下文)中,createCounter返回的值赋给了increment,变量increment现在包含一个函数定义内容为createCounter返回的函数。它不再标记为myFunction``,但它的定义是相同的。在全局上下文中,它是的标记为labeledincrement`。
- 第10行。声明一个新变量(c1)。
继续第10行。查找increment变量,它是一个函数并调用它。它包含前面返回的函数定义,如第4-5行所定义的。
- 创建一个新的执行上下文。没有参数。开始执行函数。
- 第4行。counter=counter + 1。在本地执行上下文中查找counter变量。我们只是创建了那个上下文,从来没有声明任何局部变量。让我们看看全局执行上下文。这里也没有counter变量。Javascript会将其计算为counter = undefined + 1,声明一个标记为counter的新局部变量,并将其赋值为number 1,因为undefined被当作值为 0。
- 第5行。我们变量counter的值(1),我们销毁本地执行上下文和counter变量。
- 回到第10行。返回值(1)被赋给c1。
- 第11行。重复步骤10-14,c2也被赋值为1。
- 第12行。重复步骤10-14,c3也被赋值为1。
- 第13行。我们打印变量c1 c2和c3的内容。
它并不像从我上面的解释中所期望的那样记录1,1,1。而是记录1,2,3。这个是为什么?
因为increment函数记住了那个cunter的值。这是怎么回事?
它是这样工作的,无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变量。
上面的解释都是错的,让我们再试一次,但是这次是正确的
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
- 同上, 行1 - 8。我们在全局执行上下文中创建了一个新的变量createCounter,它得到了指定的函数定义。
- 同上,第9行。我们在全局执行上下文中声明了一个名为increment的新变量。
- 同上,第9行。我们需要调用createCounter函数并将其返回值赋给increment变量。
- 同上,行1 - 8。调用函数,创建新的本地执行上下文。
- 同上,第2行。在本地执行上下文中,声明一个名为counter的新变量并赋值为 0 。
- 行3 - 6。声明一个名为myFunction的新变量,变量在本地执行上下文中声明,变量的内容是另一个函数定义。如第4行和第5行所定义,现在我们还创建了一个闭包,并将其作为函数定义的一部分。闭包包含作用域中的变量,在本例中是变量counter(值为0)。
- 第7行。返回myFunction变量的内容,删除本地执行上下文。myFunction和counter不再存在。控制权交给了调用上下文,我们返回函数定义和它的闭包,闭包中包含了创建它时在作用域内的变量。
- 第9行。在调用上下文(全局执行上下文)中,createCounter返回的值被指定为increment,变量increment现在包含一个函数定义(和闭包),由createCounter返回的函数定义,它不再标记为myFunction,但它的定义是相同的,在全局上下文中,称为increment。
- 第10行。声明一个新变量(c1)。
- 继续第10行。查找变量increment,它是一个函数,调用它。它包含前面返回的函数定义,如第4-5行所定义的。(它还有一个带有变量的闭包)。
- 创建一个新的执行上下文,没有参数,开始执行函数。
第4行。counter = counter + 1,寻找变量 counter,在查找本地或全局执行上下文之前,让我们检查一下闭包,瞧,闭包包含一个名为counter的变量,其值为0。在第4行表达式之后,它的值被设置为1。它再次被储存在闭包里,闭包现在包含值为1的变量 counter。
- 第5行。我们返回counter的值,销毁本地执行上下文。
- 回到第10行。返回值(1)被赋给变量c1。
- 第11行。我们重复步骤10-14。这一次,在闭包中此时变量counter的值是1。它在第12步设置的,它的值被递增并以2的形式存储在递增函数的闭包中,c2被赋值为2。
- 第12行。重复步骤10-14,c3被赋值为3。
- 第13行。我们打印变量c1 c2和c3的值。
是否有任何函数具有闭包,甚至是在全局范围内创建的函数?答案是肯定的。在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。
当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。
递归
如何一个函数在内部可以调用其本身,那么这个函数就是递归函数。
image.png image.png闭包优缺点:
使用闭包主要是为了设计私有的方法和变量。优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存的使用量,使用不当很容易造成内存泄漏。