JavaScript基础系列(4):如果你不理解作用域和作用域链,欢迎你点进来看看

简介: 作用域其实就是存放变量和函数的地方,全局有全局作用域,全局作用域中存放了全局变量和全局函数。每个函数也有自己的作用域,函数作用域中存放了函数中定义的变量和函数。在ES6之前,ES的作用域只有两种:全局作用域和函数作用域。ES6时支持了块级作用域。

image.png


上个月写过一篇V8是如何运行JavaScript(let a = 1)代码的?,写完之后我就发现,我对平常使用的工具V8引擎,偏底层的知识了解的竟然是如此甚少。同时我真正从事前端的时间还算是比较短的,那么基础也算是非常的薄弱。结合以上,我打算有时间就去从底层的角度去学习了解,便于在使用过程中的理解和解决遇到的问题,理解JavaScript的本质,能够更好的学习JavaScript。如果你跟我有同样的困惑,那我们可以结伴同行,共同学习。


本系列我会从我的视角不断的去总结:






前言


  • 1、作用域的概念和分类


  • 2、作用域的小demo


  • 3、执行上下文和作用域的区别


  • 4、作用域链的理解


  • 5、作用域链的小demo


  • 6、总结


1、作用域的概念和分类


通俗的说,作用域就是变量和函数的可访问范围,即作用域控制着变量和函数的可见性与生命周期。


作用域其实就是存放变量和函数的地方,全局有全局作用域,全局作用域中存放了全局变量和全局函数。每个函数也有自己的作用域,函数作用域中存放了函数中定义的变量和函数。在ES6之前,ES的作用域只有两种:全局作用域和函数作用域。

ES6时支持了块级作用域。


  • 全局作用域


其中的变量和函数在任何地方都能被访问,其生命周期伴随着页面生命周期。


  • 函数作用域


就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数的内部被访问。函数执行结束之后,函数内部定义的变量就会被销毁。


  • 块级作用域


就是使用一对大括号{}包裹的一段代码,比如函数、判断语句、循环语句等,甚至单独的一个{}都可以看做是一个块级作用域。


2、作用域的小demo


<script>
function foo() {
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b);
    }
    console.log(b);
    console.log(c);
    console.log(d);
  }
  foo()
</script>


  • 第一个断点位置

image.png


{}中声明的 var c已经在本地作用域中了,只不过初始值为undefined,从右侧的作用域可以发现,断点的位置let b此时为undefined,因为此行代码还没执行。


  • 第二个断点位置


image.png


从右侧的作用域可以发现,已经生成了{}代码块的作用域,两个let声明的变量已经赋值为undefined了,不过此时打印的话会报错,这个我之前的文章是说明原因,可以去找一下。


  • 第三个断点位置


image.png


从右侧的作用域可以发现,右侧的作用域分为了三块


  • 第一块全局作用域,其中包含了foo函数的声明


  • 第二块本地作用域,其中包含了foo函数中变量的声明


  • 第三块块级作用域(代码块),其中只包含了let声明的两个变量b和d,因为var声明的变量c存在变量提升,包含到foo函数的本地作用域中了。


块级作用域的实现是通过let和const声明的变量来支持,var声明的变量是不行的,因为var声明的变量存在变量提升


  • 第四个位置


image.png


此时可以发现控制台打印区域打印顺序为1,3,2,4


  • 1,块级作用域中没有声明a变量,于是会继续向上(foo函数)查找a变量


  • 3,块级作用域中存在变量b的声明和赋值,直接打印b的即可


  • 2,直接在函数作用域中查找b的声明,也已经赋值,直接打印即可


  • 4, c变量虽然在块级作用域中声明,但是var声明的变量存在变量提升,提升到函数级作用域,所以在此会正常打印


  • 5, d变量只在块级作用域中有声明,并且是let关键字声明的,在函数级并没有声明,在此查找d变量是找不到的,而且块级作用域在执行完毕后,在调用栈中就被移除了。所以控制台会进行报错处理d is not defined


3、执行上下文和作用域的区别


如果对执行上下文的理解还不够透彻,或许可以看看我的系列文章里的,有可能助于你的理解。


  • 作用域是在执行该函数时形成的;而执行上下文是在编译阶段,也就是代码执行前形成的。


  • 作用域相当于是静态的,函数创建后,作用域就不会变了;而执行上下文相当于是动态的,函数编译时将生成的执行上下文压入调用栈,函数执行完后移除调用栈,函数作用域随之被销毁。


  • 执行上下文就是代码的运行环境,而作用域相当于依附于执行上下文而存在。


4、作用域链的理解


如果你理解了执行上下文、调用栈、词法环境、变量环境、作用域等概念的话,再来理解作用域链可能就比较轻松了。如果你有兴趣可以看看我文章开头的第二篇和第三篇两篇文章。


在每个执行上下文的词法环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们就把这个外部引用成为outer,通过这个outer就可以继续查找并使用外部变量,这个外部的outer指向具体是如何判断的?


词法作用域: 词法作用域是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它能够预测代码在执行过程中如何查找标识符了。


通过词法作用域,也就是通过函数声明的位置,就可以确定这个outer具体的指向了,找到这个具体的指向,便可以继续查找并使用外部变量,这个链条就可以成为作用域链。


5、作用域链的小demo


<script>
function bar(){
    let test = 4
    {
        console.log(myName)
        console.log(test)
    }
}
function foo(){
    var myName ="fooName"
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myName ='全局Name'
let test = 1
foo()
</script>


先不说最终打印的结果,如果代码只运行到断点的位置(foo中执行bar函数)如下图所示


image.png


可以发现上一行代码let test = 3,在右侧单独分配了代码块作用域


代码继续执行,执行到(bar函数中的console.log位置)断点位置,如下图所示


image.png


右侧可以发现只有本地``作用域,因为代码块中不存在变量的声明,所以在右侧也就没有相应的作用域了(不像foo函数中的代码块)。


假设代码就运行到bar函数console.log的位置,现在我们就从执行上下文调用栈以及作用域链的角度去分析一下


image.png


  • 第一步全局执行上下文


代码开始运行时,V8会先初始化运行所需,准备好调用栈,然后创建全局执行上下文,并将全局执行上下文压入调用栈变量环境中存储var声明的变量myName,词法环境中存储let和const声明的变量test,词法环境中同时存在一个类似外部指向的outer,通过这个指向来查找并使用变量,全局执行上下文中的outer指向是null,因为全局执行上下文是最大的上下文了,没有外部了。


  • 第二步foo函数执行上下文


准备好全局执行上下文之后,就开始执行可执行代码了,于是找到了foo()。此时开始创建foo函数的执行上下文,同时会将函数执行上下文压入调用栈,同样的:变量环境中存储var声明的变量myName,词法环境中存储let声明的变量test,词法环境中同时存在一个类似外部指向的outer,根据词法作用域也就是函数声明的位置可以发现,这个outer指向了全局执行上下文


然后还有一块{}包裹的代码块,在词法环境中,这里会形成一个小型栈结构,栈底就是foo函数中声明的test=2,然后进入块级作用域后,会将块级作用域中的let=3压入栈顶,因为都是let声明的变量,所以这个小型的栈结构,发生在词法环境中,同理如果有var声明的则会在变量环境中。


注意:块级作用域的实现是通过let或者const声明的变量来支持的。这里的块级作用域是无需压入调用栈的。


  • 第三步bar函数执行上下文


然后继续执行foo函数中的bar函数调用,此时便开始创建bar函数的执行上下文。同样的:词法环境中存储let声明的变量test,此函数中没有var声明的变量,所以变量环境中为空。


然后开始执行{}代码块中的console.log打印函数。


先说打印test变量: test变量在块级作用域中没有声明,于是会到函数作用域中进行查找,发现bar函数执行上下的词法环境中存在test变量,便进行打印4。


再来打印myName变量:myName变量在块级作用域中没有声明,于是继续会到函数作用域中进行查找,发现bar函数作用域中也不存在,于是通过作用域链,根据outer外部引用,找到全局执行上下文中的myName="全局Name"


注意: 作用域链的查找过程,一定要根据词法作用域


总结


  • 作用域的分类:全局作用域、函数作用域、块级作用域


  • 块级作用域是通过let或者const声明的变量来支持块级作用域的,var声明的变量存在变量提升,是不支持块级作用域的


  • 执行上下文和作用域的区别


  • 注意一下词法作用域的定义:根据代码在函数中声明的位置来决定的


  • 作用域链就是根据词法作用域来查找变量的
目录
相关文章
|
10天前
|
JavaScript 前端开发 开发者
JavaScript的变量提升是一种编译阶段的行为,它将`var`声明的变量和函数声明移至作用域顶部。
【6月更文挑战第27天】JavaScript的变量提升是一种编译阶段的行为,它将`var`声明的变量和函数声明移至作用域顶部。变量默认值为`undefined`,函数则整体提升。`let`和`const`不在提升范围内,存在暂时性死区。现代实践推荐明确声明位置以减少误解。
17 2
|
16天前
|
存储 JavaScript 前端开发
第五篇-Javascript作用域
第五篇-Javascript作用域
19 2
|
19天前
|
JavaScript 前端开发
JavaScript 作用域
JavaScript 作用域
16 2
|
2月前
|
JavaScript 前端开发
JavaScript 闭包:让你更深入了解函数和作用域
JavaScript 闭包:让你更深入了解函数和作用域
|
12天前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包是函数访问外部作用域变量的能力体现,它用于封装私有变量、持久化状态、避免全局污染和处理异步操作。
【6月更文挑战第25天】JavaScript闭包是函数访问外部作用域变量的能力体现,它用于封装私有变量、持久化状态、避免全局污染和处理异步操作。闭包基于作用域链和垃圾回收机制,允许函数记住其定义时的环境。例如,`createCounter`函数返回的内部函数能访问并更新`count`,每次调用`counter()`计数器递增,展示了闭包维持状态的特性。
27 5
|
10天前
|
JavaScript 前端开发
JavaScript作用域关乎变量和函数的可见范围。
【6月更文挑战第27天】JavaScript作用域关乎变量和函数的可见范围。全局作用域适用于整个脚本,局部作用域限于函数内部,而ES6引入的`let`和`const`实现了块级作用域。全局变量易引发冲突和内存占用,局部作用域在函数执行后消失,块级作用域提高了变量管理的灵活性。作用域关键在于组织代码和管理变量生命周期。
18 1
|
12天前
|
JavaScript 前端开发
JavaScript中的变量提升(Hoisting)将`var`声明和函数声明提前到作用域顶部,允许在声明前使用
【6月更文挑战第25天】JavaScript中的变量提升(Hoisting)将`var`声明和函数声明提前到作用域顶部,允许在声明前使用。`let`和`const`不完全提升,存在暂时性死区(TDZ),尝试在初始化前访问会出错。函数声明会被提升,但函数表达式不会。
17 3
|
18天前
|
JavaScript
Vue.js中使用.self修饰符来限制事件处理程序的作用域
Vue.js中使用.self修饰符来限制事件处理程序的作用域
|
20天前
|
自然语言处理 JavaScript 前端开发
【JavaScript】JavaScript基础知识强化:变量提升、作用域逻辑及TDZ的全面解析
【JavaScript】JavaScript基础知识强化:变量提升、作用域逻辑及TDZ的全面解析
19 3
|
23天前
|
自然语言处理 JavaScript 前端开发
深入了解JS作用域
深入了解JS作用域