哈喽友友们,我又来了!
今天我们只有两个目标:搞懂什么是js的作用域链?什么是js的闭包?在进入内容之前,为了更好的接受本篇内容,可以先回顾一下我之前写的两篇文章:
- 关于js作用域的文章: 搞定一个js知识点——全局、函数和块级作用域- 掘金
- js的执行上下文的文章:深挖js的执行上下文:为什么函数可以访问全局的变量?- 掘金
话不多说,咱们进入正题!
解决声明提升
了解js中的var关键词的人明白,var定义的变量存在声明提升,所以es6为了解决这个问题,就设计了let和const关键词。(还有不了解的可以看我前面提的关于js作用域的文章)
我们复习一下,简单看下面一段代码:
function varTest(){
var x = 1
if(true){
var x = 2
// let x = 2
console.log(x); // 输出2
}
console.log(x); // 输出2
}
varTest();
我们知道:代码中的if语句内部不会形成块级作用域,在调用栈中,js会将变量x储存在varTest函数执行上下文的变量环境,而var会重新定义x并覆盖原值,所以x为2,两次打印都是2。如图:
如果把第四行代码中的var改成let,如第五行注释,那let + {}会形成块级作用域,第一个输出会是2,第二个输出会是1。那在执行上下文中有两个变量x,肯定不会在同一个地方,那块级作用域的x会存在哪里呢?
其实js将块级作用域的变量x储存在varTest函数执行上下文的词法环境中,所以第一个打印是写在块级作用域的,先查找块级作用域的变量x=1,最后是函数作用域的变量x=2,如图:
但是,在词法环境中存储变量还有一个小细节,我们再看一段代码:
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a); // 输出1
console.log(b); // 输出3
}
console.log(b); // 输出2
console.log(c); // 输出4
console.log(d); // 输出undefined
}
foo();
运行代码,我们会发现,第八行和第十一行输出的不一样。其实我们都知道let不能重复定义变量b,而且一个是打印块级作用域的b,一个是打印函数作用域的b。但是我们说let声明的变量都在foo函数执行上下文的词法环境中,怎么区分这两个b呢?
其实,执行上下文对象的词法环境是在维护一个新的栈,let和const声明的变量会存在里面;每一个块级作用域会是一个小栈进入其中,各自放块级作用域里声明的所有变量,并且也是会销毁的。如图,就是上面代码的foo函数执行上下文的示意图。特别注意的还是c,虽然是在花括号里面的,但是它是var定义的,所以不是在块级作用域里面的。
作用域链
1. 词法作用域规则
在之前的文章中,我们说调用栈中的执行上下文从上往下访问,其实这是不完全正确的。举个例子:
function bar(){
console.log(myname); // 输出:墩墩
}
function foo(){
var myname = '美美'
bar()
console.log(myname); // 输出:美美
}
var myname = '墩墩'
foo()
我们根据代码画出各个执行上下文示图:
我们发现不对劲了,如果是bar函数内部打印myname的话,bar函数执行上下文就应该直接从上往下查找变量会输出“美美”,但是它不是,输出的是“墩墩”。所以调用栈中的执行上下文不是盲目的从上往下访问。
这我们就要引入一个新的概念——词法作用域。有些地方会叫词法环境,但是不是执行上下文内部那个词法环境,要与之去区分开。
如果说函数作用域相当于是函数自己的空间,那么词法作用域就是整个函数所在的那个空间。所以函数的词法作用域就是函数被定义在的那个域。在每个执行上下文中存在一个outer属性,指向的是当前执行上下文的下一级,就是函数的词法作用域;全局的outer指向null。
函数访问内部声明的变量在函数作用域中找,访问内部未声明的变量会在词法环境里找。
所以如图,在代码中,bar函数的声明在全局,outer指向全局执行上下文,bar函数访问未在函数体内声明的变量myname会在全局执行上下文中访问,打印“墩墩”;而foo函数则访问自己内部的myname,打印“美美”。
2. 作用域链
什么是作用域链?直接上代码更好理解:
let count = 1
// main 的outer 指向全局
function main(){
let count = 2
// bar 的outer 指向main
function bar(){
let count = 3
// foo 的outer 指向bar
function foo(){
let count = 4
}
foo()
}
bar()
}
main()
首先,我们先分析代码,你能画出它的调用栈的执行上下文吗?如图:
如果,我们把给个函数的count变量删掉,只留下全局的count,最后再在foo函数体内打印count,我们会得到1。画出了图,我们能清晰的摸清变量查找的顺序:foo函数作用域内部 -> bar函作用域 -> main函数作用域内部 -> 全局作用域内部。
所以,V8在查找变量过程中,顺着执行上下文中的 outer指向 查清一整根链,这种链状关系就叫作用域链。
闭包
1. 闭包的概念
在js中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。当通过调用一个外部函数返回的一个内部函数时,即使外部函数调用结束了,但是内部函数引用了外部函数中的变量,那么这些变量依然需要保存在内存中,我们把这些变量的集合成为闭包。
一大段字是不是有点抽象了。哈哈哈哈~
还是结合代码来解释吧:
function foo(){
function bar(){
var a = 1
console.log(b);
}
var b = 2
return bar // 不是调用,把函数返回了
}
const baz = foo() // 输出函数体赋值给baz
baz() // 输出2
我们还是画出这段代码运行时的调用栈变化过程:
在代码中,foo函数返回一个内部定义的bar函数,而这个内部函数被赋值给了baz。如图右边调用栈,函数foo执行完毕,它的执行上下文销毁;baz执行,即bar函数调用了,触发创建执行上下文。根据词法作用域规则,bar函数声明在foo内部,bar访问变量b应该是在foo函数内部访问,但是foo函数在执行完的一刻它的执行上下文已经销毁了,这完全起冲突了,js又是怎么访问成功的呢?
其实,是因为函数的执行上下文没有销毁的很彻底,它还留下来一个小小的背包里面储存着变量b,bar才能访问到b。
所以,是js为了解决词法作用域的规则 和 函数调用完毕它的执行上下文一定会被销毁这一规则的冲突,就创建了闭包,也叫函数的背包,就是专门用来存储被内部函数引用了外部函数中的变量。
2. 闭包的优缺点
- 闭包的缺点——内存泄漏:闭包写的越多,调用栈的可用空间越来越少(每次销毁都没有销毁干净,会导致爆栈)
- 闭包的优点——可以实现变量的私有化,封装模块
这里我们举个例子,说明闭包能够实现变量的私有化。
function fn(){
var arr = []
for(var i = 0; i < 5; i++){ // 循环结束,i = 5
arr.push(function(){
console.log(i)
}
}
}
return arr
}
var funcs = fn()
for(var j = 0; j < 5; j++){
funcs[j]() // 数组中的五个函数依次执行,输出5 5 5 5 5
}
上述代码的运行结果是五个5,如果我们想要每次的i也存起来,并在最后数组五个函数依次输出时打印1 2 3 4 5,你会怎么办?
代码中可以看出,fn其实是一个外部函数,arr数组的每一项都是fn函数的内部函数。但是fn执行完毕后只留下了一个闭包存储着变量i=5;在内部函数中,都是访问i,所以打印全是一个i都是5。
所以我们直接再使用闭包来完成,让i私有化:我们让i成为arr数组中的每一个函数的外部函数的闭包中的变量。即我们要让arr内的每个函数在返回在数组之前作为一个函数的内部函数,并且每一次循环,外部函数执行完毕,产生一个闭包存储内部函数引用的i,那第一次就是0,第二次就是1,第三次就是2......就解决了。
代码如下:
function fn(){
var arr = []
for(var i = 0; i < 5; i++){ // 循环结束,i = 5
// 把数组内部函数写在一个有参数的自执行函数里面,n 与 i 数值相等,参数n会在内部函数调用时引用,所以不会完全销毁,存在闭包里
// 每次循环会执行结束会产生一个该函数的闭包,分别存了n = 0;n = 1;n = 2;n =3;n =4
(function(n){
arr.push(function(){
console.log(n)
})
})(i)
}
return arr
}
var funcs = fn()
for(var j = 0; j < 5; j++){
funcs[j]() // 数组中的五个函数依次执行,i = 5
}
// (function(){})() 是自执行函数
所以我们就让每次进入函数的不是最后的i,并使得外部函数的参数与内部参数的值一致,但是每次循环传入一次,就能够得到五个闭包,arr在执行时也能依次访问五个闭包的变量。
好了,今天的内容就分享到这里,希望你也懂了!
喜欢的话点个赞吧❥(^_-)
那我们下期再见