前言
很多人都认为闭包很难,其实不然,只要我们理解也会感觉闭包好像也就这么回事, 在直接介绍闭包之前我们需要去了解两个概念来加深我们对闭包更底层的理解:调用栈 ,作用域链。
调用栈
调用栈(Call Stack)是一种数据结构,用于跟踪函数的执行顺序。当执行JavaScript代码时首先会生成一个全局上下文执行对象GO,以及编译函数的时候也会生成一个函数的上下文执行对象AO,这些上下文执行对象会被推入调用栈中。这些上下文执行对象是按照先进后出原则执行,执行完毕后会出栈。
调用栈指针:调用栈指针通常是指当前正在执行的代码所在调用栈的位置。这个指针会随着函数的调用和返回而不断移动,指向当前正在执行的函数上下文。
不是理解两种执行对象的小伙伴可以去翻我的之前的文章(当面试被问到js预编译原理 ?!别急,也许你可以回答得更好(后附面试题) - 掘金 (juejin.cn))
作用域链
我们知道在函数的执行过程当中,如果要对一个函数的作用域内不存在的变量进行赋值,那么引擎就会一直往外层的作用域去寻找,直到找到该变量声明或者到全局作用域创建一个全局变量。为什么引擎可以这样做呢?这里便是我们要探究的概念,作用域链。
在生成上下文执行对象的时候,会自动根据该对象的词法作用域位置生成一个outer 属性,这个属性值就是该对象作用域的外层作用域 全局上下文执行对象outer 的默认值为null 因为全局作用域已经是整个函数的最外层了。而其它函数的上下文执行对象的outer值会根据该对象所在的词法作用域来生成。
作用域链
- 通过词法作用域来确定某作用域的外层作用域,查找变量由内而外的这种链状关系叫做作用域链
- outer 全局作用对象 词法环境有outer=null 函数作用对象 outer=外部作用域
js
var a=2
function add(b,c){
return b+c
}
function addall(b,c){
var d=10
result=add(b,c)
return a+result+d
}
console.log(addall(3,6))
图中这些红色箭头表示的链状关系便是作用域链,函数add执行对象outer的值为全局作用域,函数addall执行对象的outer值也为全局作用域,而全局上下文执行对象的outer值为null。也更底层地解释了为什么在执行函数addall时在函数内部的执行作用域找不到a则会去全局作用域找。
闭包
了解了预备知识我们开始正题,先通过下面的这段代码来引入闭包:
以下代码的执行结果是什么
js
function foo() {
var myName='张三'
let test1 = 1
let test2 = 2
var innerBar={
getName:function() {
console.log(test1)
return myName
},
setName:function(newName) {
myName=newName
}
}
return innerBar
}
var bar=foo()
bar.setName('李四')
console.log(bar.getName())
分析
- 首先编译记录声明语句foo ,bar
- 执行将foo() 编译并将其返回值赋值给bar ,返回值是一个函数innerBar
- 调用函数的setName 方法将myName的值从"张三"改为"李四"
- 使用函数的getName 方法获取到myName值为"李四",输出李四
奇怪的地方
但是如果你知道上面调用栈的知识就会感觉不对,因为调用foo 当中函数中innerBar 函数时,foo() 执行完已经出栈了,那么innerBar 应该也已经出栈了,那么也就不能再调用innerBar函数的方法了。为什么会没有报错,还能继续调用以及出栈的函数呢?
理解
在执行完 bar=foo() 这条语句后foo的执行对象也就出栈,但实际上它的返回值innerBar 这个函数并不确定有没有执行完。那么对于这种情况,调用栈会将作为返回值返回出去的函数体 保留在调用栈内,以及该函数使用到的外函数的变量 也会一一保留下来,也就是说foo的执行对象并没有完全出栈,这些使用到的外部函数变量以及函数体会一直存放在调用栈内,简单说就是说函数执行对象出栈的时候,如果有不受外函数管制的内部函数,出栈时会保留该内部函数在调用栈内存中,以及它所使用的外函数变量,这个函数体和它使用的外部函数变量的这个集合称为闭包。
使用
使用场景应用场景变量私有化,不能有全局变量情况,在企业级开发中代码总量非常大使用全局变量的话根本分不清哪里调用了,就要使用闭包。以下这段代码能够体现闭包变量私有化的作用。
js
var arr = [];
for (var i = 0; i < 10; i++) {
arr[i] = function a(){
console.log(i);
}
}
for (var j = 0; j < arr.length; j++) {
arr[j]()//输出10个10
}
以上代码中当我们再次调用arr 中每个元素存放的函数a时都会执行console.log(i),但是i是全局变量,在循环结束之后值为10,所以打印的是10,那么有什么办法解决该问题吗?在不改变数组中存放函数的前提下,闭包便是一种很好的选择。
js
var arr=[]
for(var i=0;i<10;i++){
//立即调用函数
(function a(j){//j为形参
arr[i]=function b(){//函数b被赋值出去,构成10个闭包,记录每次循环时变量的值,再次调用时会读取该闭包内的变量
console.log(j)
}
})(i)//传入实参
}
for(var j=0;j<arr.length;i++){
//即便函数a出栈但是闭包(函数体b,变量)还在调用栈内
arr[j]()//成功打印数字0-9
}
缺点
调用栈的空间是有限的
超出栈最大内存报错Uncaught RangeError: Maximum call stack size exceeded
闭包会导致 "内存泄露"(该函数体以及相应的变量一直存储在调用栈空间内没有出栈),过多使用闭包也可能会导致调用栈内存不足而报错。