作用域链和闭包
我们先来看一段代码
function bar() {
console.log(myName)
}
function foo() {
var myName = " 极客邦 "
bar()
}
var myName = " 极客时间 "
foo()
根据我们之前学过的知识,现在我们可以通过上下文来分析代码的执行流程了,其调用栈的状态图如下:
现在全局执行上下文和foo函数的执行上下文中都有变量myName,那么在打印的时候我们应该优先选择哪个?
如果按照上一节词法环境的查找,会从栈顶开始查找,如果我们的变量环境也按照这种方式查找,那么打印出来的就应该是极客邦
但是事实并非如此,打印出来的应该是极客时间
作用域链
其实每个执行上下文的变量环境中,都包含一个外部引用,用来指向外部的执行上下文,把这个外部引用称为**outer**
所以现在查找一个变量,会先在当前的执行上下文中查找该变量,如果没有找到则查找outer所指向的执行上下文,如图所示(这里不考虑let的情况)
这里可以看到bar和foo的外部变量指向的都是全局执行上下文,那为什么是指向全局执行上下文呢,这里就要了解词法作用域
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预判代码在执行过程中如何查找标识符
可以看到,词法作用域链为:foo函数作用域 -> bar函数作用域 -> main函数作用域 -> 全局作用域
所以很自然,前面的foo和bar函数的上级作用域是全局作用域,所以外部变量指向的应该是全局作用域
词法作用域是代码阶段就决定好了,和函数怎么调用并没有关系
块级作用域中的变量查找
这里再看一段代码:
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()
分析这一段代码的输出:
这是函数执行到打印处的调用栈状态,现在我们就可以开始查找变量了,先在词法环境中自顶向下查找,找不到就去本函数的上下文中的变量环境查找,再找不到就去该函数的outer外部变量指向的作用域中找,也是由词法环境找到变量环境,也就是按照上图的1、2、3、4、5的顺序查找
闭包
我们先来看一段代码:
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())
根据调用栈的相关知识,我们可以看到,当代码执行到return innerBar的时候调用栈的情况:
根据词法作用域的规则,内部函数getName和setName可以访问外部函数foo中的变量,所以innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是上面两个内部函数还是可以使用foo函数中的变量myName和test1,所以foo函数执行完后,整个调用栈状态如下:
foo执行完后,其执行上下文会从栈顶弹出,但是由于返回了innerBar其中包含着两个内部函数,还使用了foo函数中的变量myName和test1,所以这个变量依然保持再内存中,所以这就像是一个foo函数的专属背包
由于只有两个内部函数能访问该背包,其他地方无法访问,所以我们就把这个背包称为闭包
所以现在我们可以给闭包一个定义:在JS中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在内存中,我们把这些变量集合称为闭包
知道闭包的定义之后,我们再来继续看一下上述代码的执行过程,现在执行到bar.setName(" 极客邦 "),现在调用栈的状态应该变成:
现在要查找myName变量,就会先从setName函数中查找,如果没有就会查找闭包中的myName变量,找到后会修改闭包中的myName变量的值
闭包回收
如果引用闭包的函数是一个全局变量,那么闭包会一直存在到页面关闭,但如果这个闭包以后不会使用的话,就会造成内存泄漏
如果引用闭包的函数是一个局部变量,那么函数销毁后,下次JS引擎执行垃圾回收时,如果闭包这块内容不用再被使用的话,那么JS的垃圾回收机制就会回收这块内存
所以使用闭包要注意一个原则:如果闭包会一直使用,那么就让他作为全局变量存在,如果使用频率不高,内存占用又比较大的时候,就尽量让其成为一个局部变量