如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链作用域链的概念.
理解作用域链是理解闭包的基础.
什么是作用域链?
function foo(){
console.log(test);
}
function foo2() {
var test="1";
foo();
}
var test="2";
foo2();
// 很显然结果是2 为什么是2?分析下过程
/*
执行上下文为
foo上下文 变量环境:可执行代码 console.log(test) 打印的到底是谁?
foo2上下文 变量环境:test="1"
全局上下文 变量环境:test="2"
从代码中可以看到全局作用域和foo2作用域都有定义test
那foo 到底找到的是哪个test?
很多人第一反应是延调用栈去找,这是不对的!。
先foo->foo2->全局 这是不对的!
*/
作用域链
关于作用域链,很多人会感觉费解,但如果你理解了调用栈、执行上下文、词法环境、变量环境等概念,那么你理解起来作用域链也会很容易。
其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer。
当一段代码使用了一个变量时,JavaScript引擎首先会在“当前的执行上下文”中查找该变量比如上面那段代码在查找test变量时 没找到就会使用变量环境指向的outer继续查找
foo 的outer指向 全局执行上下文 foo2的outer指向 全局执行上下文 全局 的outer指向 null
foo、foo2 outer 都指向全局执行上下文,变量也会顺着去查找,我们称这个查找的路径为作用域链。
为什么foo的outer不是foo2? 要回答这个问题,你还需要知道什么是词法作用域词法作用域。这是因为在JavaScript执行过程中,其作用域链是由词法作用域决定的。
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。能够预测代码在执行过程中如何查找标识符
从图中可以看出,词法作用域就是根据代码的位置来决定的,其中main函数包含了bar函数,bar函数中包含了foo函数,因为JavaScript作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo函数作用域—>bar函数作用域—>main函数作用域—>全局作用域。
回答上面的问题 为什么foo的outer不是foo2? 因为词法作用域的存在,foo foo2都是定义在全局所以在解析的时候就决定了,他们的outer是全局作用域。AST SCOPE阶段
词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系数是怎么调用的没有
块级作用域中的变量查找
function bar() {
var myName = "a"
let test1 = 100
if (1) {
let myName = "b"
console.log(test)
}
}
function foo() {
var myName = "c"
let test = 2
{
let test = 3
bar()
}
}
var myName = "d"
let myAge = 10
let test = 1
foo()
test 的查找路径 bar的if 块级词法环境->bar的词法环境->bar的变量环境outer->全局执行上下文词法环境->全局执行上下文的变量环境。
查找先词法在变量在outer
什么是闭包? 解了变量环境、词法环境和作用域链等概念,那接下来你再理解什么是JavaScript中的闭包就容易多了
function foo() {
var myName = "a"
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("b")
bar.getName()
console.log(bar.getName())
根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1。
当foo执行结束的时候,myName、test1,会被添加到closure中来,实际上这个地方会在预计解析的时候就创建了地址空间然后把myName,test1创建在这个空间里面了.
foo函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的setName和
getName方法中使用了foo函数内部的变量myName和test1,所以这两个变量依然保存在内存中。这像极了setName和getName方法背的一个专属背包,无论在哪里调用了setName和getName方法,它们都会背着这个foo函数的专属背包之所以是专属专属背包,是因为除了setName和getName函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为foo函数的闭包闭包
根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
比如外部函数是foo,那么这些变量的集合就称为foo函数的闭包比如外部函数是foo,那么这些变量
setName的作用域链是什么? 当前执行上下文 -> 闭包 -> 全局上下文。
闭包是怎么回收的
在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存如果该闭包会一直使用,那么它可以作为全局变量而存 在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
总结 介绍了什么是作用域链,我们把通过作用域查找变量的链条称为作用域链;作用域链是通过词法作用域来确定的,而词法作用域反映了代码的结构。
介绍了在块级作用域中是如何通过作用域链来查找变量的。
作用域链和词法环境介绍了到底什么是闭包
作用域链是已经注定好了,比如即使在foo函数中调用了bar函数,你也无法在bar函数中直接使用foo函数中的变量信息。