上一篇文章我们分析了块级作用域是 通过创建执行上下文中变量环境和词法环境来实现的,这次我们来分析一下作用域链的概念
作用域链是理解闭包的基础,且闭包在JavaScript中无处不在,接下来一起看看什么是 作用域链 ,然后通过作用域链进而掌握 闭包的概念
话不多说,看下面一段代码:
function bar() {
console.log(myName)
}
function foo() {
var myName = "极客邦"
bar()
}
var myName = "极客时间"
foo()
这段代码在执行的时候会输出极客邦还是极客时间呢?自己动手试一下
下面来根据执行上下文的理论分析一下结果,当函数执行到bar的时候,调用栈的状态图如下:
在上图中可以看到,foo函数的作用域和全局作用域都有myName变量,那么查找myName的顺序是如何呢?是根据调用栈顺序从上向下么,这样的情况那么应该输出极客邦,然后答案却不是我们想的那样,究竟是怎么查看变量的呢?
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,这个外部引用通常被称为outer, 当一段代码执行的时候,首先会在当前的执行上下文中去查找,如果没有找到,那么JavaScript引擎会继续在outer所指向的执行上下文中查找,示意图如下:
从上图可以看出,bar函数和foo函数的outer都是指向全局的,这说明如果在当前执行上下文中没有找到变量,则会去全局执行上下文查找, 这个查找的链条就称为作用域链
貌似还有一个疑问没解决 – 为什么执行上下文中的outer会指向全局?,要回答这个问题,还需要知道什么是词法作用域,JavaScript执行过程中,作用域链是由词法作用域决定的。
词法作用域是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符,看下面一张图
由上图可以看出,词法作用域是由代码的位置决定的。回头看一下之前的问题,foo函数调用了bar函数,为什么bar函数的外部引用是全局执行上下文,而不是foo函数执行上下文呢?
这是因为根据词法作用域,foo和bar的上级作用域都是全局作用域,所以如果foo或者bar函数使用了一个它们没有定义的变量,那么就会去找outer执行上下文,也就是说词法作用域是代码编译阶段就决定好的,和函数怎么调用没有关系
前面我们分析了通过全局作用域和函数作用域分析了作用域链,接下来看一下块级作用域是如何查找的?看一下下面的代码:
function bar() {
var myName = "极客世界"
let test1 = 100
if (1) {
let myName = "Chrome浏览器"
console.log(test)
}
}
function foo() {
var myName = "极客邦"
let test = 2
{
let test = 3
bar()
}
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()
下面我们通过作用域链的概念来分析一下这段代码的执行过程:
ES6支持块级作用域,如果遇到let或者const定义变量的时候,会把该变量放到词法环境中,对于上面的代码,当执行到bar函数中if语句的时候,调用栈的情况如下图所示:
理解了变量环境,词法环境和作用域链等概念,接下来再理解闭包就容易多了,看一下下面的代码:
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
来看一下代码执行到foo函数内部return innerBar这行代码时调用栈的情况:
上面的代码可以看出,innerBar是一个对象,包含两个方法,两个方法都是在foo函数内部定义的,并且两个方法内部都使用了myName和test1变量
根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo的变量, 当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1,foo函数执行完成以后,整个调用栈的状态如下:
从上面可以看出:foo函数执行结束后,其执行上下文从栈顶弹出了,但是由于返回的setName和getName函数中引入了foo函数内部的变量myName和test1,所以这两个变量依然保存在内存中,
来看一下闭包的概念:
当执行到bar.setName方法中的myName = ‘极客帮’时,JavaScript引擎会沿着"当前执行上下文 -> foo函数闭包 -> 全局执行上下文" 的顺序来查找myName变量,具体调用栈如下:
从上面可以看出,setName的执行上下文中没有myName变量,foo函数的闭包中存在变量myName,所以调用setName时会修改foo闭包中的myName变量的值。
同样的流程,调用bar.getName时,访问的变量myName也是位于foo函数的闭包中
理解了什么是闭包之后,接下来我们再看一下闭包是什么时候被销毁的,因为如果闭包使用不正确,很容易造成内存泄漏
如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭,但是这个闭包以后如果不再使用,那么就会造成内存泄漏
如果引用闭包的函数是个局部变量,等函数销毁之后,在下次JavaScript引擎执行垃圾回收的时候,判断闭包的这块内容如果不再使用,那么JavaScript引擎的垃圾回收器就会回到这块内存
在使用闭包的时候,要尽量遵循一个原则:如果该闭包会一直使用,那么它可以作为一个全局变量而存在,如果使用频率不高,而且占用内存又比较大,那么久尽量让它成为一个局部变量