从零开始讲解JavaScript中作用域链的概念及用途

简介: 之前我写过一篇关于JavaScript中的对象的一篇文章,里面也提到了作用域链的概念,相信大家对这个概念还是没有很深的理解,并且这个概念也是面试中经常问到的,因为这个概念实在太重要了,在我们平时写代码时,也可能会因为作用域链的问题,而出现莫名其妙的bug,导致我们花费大量的时间都查找不出原因。所以我就准备单独写一篇关于作用域链的文章,来帮大家更好地理解这个概念。

01

执行环境


首先,我们要引入一个概念,叫做执行环境(下面简称环境)。在一个执行环境中,有一个与之关联的变量对象(下面简称对象),在该对象中,储存着这个执行环境中定义的变量和函数。但这个对象只是个形式上的对象,并不能被外界所访问到。


例如,在浏览器中,我们在全局运行下列代码,那么当前的执行环境就是window,也就是全局,并且与该全局环境关联的对象中存储着定义的变量fruit


    <!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>Title</title></head><body>    <script>        let fruit = 'banana'        alert(fruit)</script></body></html>



    那么,在javascript中,函数也会形成一个环境,例如下列的代码中,函数的内部就是一个局部的环境,与该环境关联的对象中存储着变量my_favorite;而此时全局环境window中,也有一个与之关联的对象,该对象中存储着变量fruit 和函数 fn1


      <!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>Title</title></head><body>    <script>        let fruit = 'banana'        alert(fruit)        function fn1() {            let my_favorite = 'apple'            return my_favorite        }        fn1()</script></body></html>


      02

      作用域链


      看了上面两个例子,我们对执行环境应该有了一定的了解,那么这里就将引入作用域链的概念了,当代码执行在一个环境中时,会针对环境中储存变量和函数的对象创建一个作用域链,作用域链的最前端就是当前环境的对象,如果当前环境是个函数,则作用域链的下一部分就是全局的window环境的变量对象。


      先来看一下代码部分


        <script>  let fruit = 'banana'  function fn() {    let color = 'red'    //返回  '我最喜欢的水果是banana,我最喜欢的颜色是red'    console.log('我最喜欢的水果是' + fruit + ',我最喜欢的颜色是' + color)  }  fn()  //报错, color is undefined  console.log('我最喜欢的水果是' + fruit + ',我最喜欢的颜色是' + color)</script>


        首先执行了函数 fn ,此时函数内的作用域链就是这样的


        f90e8815eefb83d805622e0e5dbbdb9a.png


        我们看到,在函数 fn 中,我们使用了变量 fruitcolor,所以此时会从作用域链的头部开始,从第一个活动变量(本例中第一个变量对象就是函数fn的活动变量)中,寻找变量 fruitcolor,发现该变量对象中存在变量color,于是就成功引用了变量color但是因为没有找到变量 fruit,于是再沿着作用域链往下找到下一个变量对象(本例中第二个活动变量就是全局window的变量对象),发现该变量对象中有我们想要的变量 fruit,则引用该变量 fruit ,同时,因为找到了需要引用的变量,就不会继续沿着作用域链继续向下寻找了。


        我们再来看在函数外,也就是全局window中,也执行了console.log('我最喜欢的水果是' + fruit + ',我最喜欢的颜色是' + color),此时在全局环境中的作用域链是这样的


        c6292fdfd09946aa0a373c22d87be00d.png


        此时也使用了变量 fruitcolor,所以这时会从作用域链的头部开始,找到第一个变量对象(本例中第一个活动变量就是window全局变量对象),发现该变量对象中有变量 fruit,所以成功引用该变量对象中的 fruit,但因为没找到变量 color,所以继续沿着作用域链向下寻找下一个活动变量,但此时已经找到了作用域链的尾部,并没有别的变量对象了,所以也就无法找到变量 color了,所以最后返回的就是 undefined。在本例中我们可以看到,当代码处于全局环境中时,是没有访问函数fn执行环境中的变量color的权力的,这里我们可以这种现象看成是变量color作用域只是在函数fn的执行环境内。


        这就是代码执行时,作用域链起到的作用,所以作用域链就保证了执行环境中代码对变量的有序访问。



        03

        块级作用域


        在JavaScript中是没有块级作用域的,也就是说,由花括号或小括号封闭起来的区域内没有自己的作用域,例如这两个例子


          if(true) {  var fruit = 'banana'}console.log(fruit)    //返回  banana


          我们可以看到,if 语句中的花括号内,使用 var 定义了一个变量 fruit,按照作用域链的规则来说,外部是无法访问到该变量的,但是我们可以看到,确实返回了这个变量的值 banana


          再来看下一个例子


            for(var i=0; i<4; i++) {  alert(i)}console.log(i)    //返回4


            在使用 for语句时,我们在小括号里使用var定义了一个临时变量i,同样的的,在 for循环结束以后,在外部访问该变量,也成功返回了相应的值。


            以上两个例子,都是因为JavaScript没有块级作用域引起的,所以有时会因为这种情况,导致一些不必要的麻烦。在ES6中,出现了使用 letconst声明变量的方式,来解决了JavaScript中没有块级作用域的问题。


            04

            其他情况


            其实,还有一种情况,会影响变量的访问顺序,那就是在声明变量时,直接给一个未声明的变量赋值,例如这样


              function fn() {  sum = 1 + 2}fn()
              console.log(sum)      //返回 3


              按照我们本文前面讲解的作用域链的知识,当执行到最后一局代码时,此时处于全局执行环境中,查询不到变量 sum,所以应当会报错 undefined,但这里却返回了 3。


              这是因为,在我们使用var声明变量时,会自动将该变量放到离该代码最近的活动变量中去,也就是函数fn的活动变量中,所以在全局执行环境中的代码就无法访问到该变量。但是如果不使用var,而是像这个例子中一样,直接给一个未定义的变量赋值,这时会自动地将该变量放到全局的活动变量中去,这就是导致本例中在全局环境中还能访问到变量sum的原因。


              05

              总结


              1. 作用域链可以看成是将变量对象按顺序连接起来的一根链子


              1. 每个执行环境中的作用域链都是不同的


              1. 当我们引用变量时,会顺着当前执行环境的作用域链,从作用域链的开头开始依次往下寻找对应的变量,直到找到作用域链的尾部,报错undefined


              1. 作用域链保证了变量的有序访问


              相关文章
              |
              3月前
              |
              设计模式 JavaScript 前端开发
              在JavaScript中,继承是一个重要的概念,它允许我们基于现有的类(或构造函数)创建新的类
              【6月更文挑战第15天】JavaScript继承促进代码复用与扩展,创建类层次结构,但过深的继承链导致复杂性增加,紧密耦合增加维护成本,单继承限制灵活性,方法覆盖可能隐藏父类功能,且可能影响性能。设计时需谨慎权衡并考虑使用组合等替代方案。
              40 7
              |
              8天前
              |
              JavaScript 前端开发
              js bom的概念
              js bom的概念
              17 1
              |
              16天前
              |
              JavaScript 前端开发
              JavaScript BOM 的概念(浏览器对象模型)
              JavaScript BOM 的概念(浏览器对象模型)
              27 1
              |
              19天前
              |
              JavaScript 前端开发
              JavaScript 事件的概念
              JavaScript 事件的概念
              37 1
              |
              20天前
              |
              JavaScript 前端开发 Java
              JavaScript中的闭包概念讲解
              闭包是指函数内部嵌套另一个函数,并且内部函数引用了外部函数的数据(如变量或函数)。这样的内部函数被称为闭包。以示例代码为例,`fn1` 中有两个闭包函数 `fn2` 和 `fn3`,它们都可以访问并修改 `fn1` 中的变量 `a`。
              11 1
              |
              3月前
              |
              JSON JavaScript 前端开发
              【JavaScript】JavaScript中的深拷贝与浅拷贝详解:基础概念与区别
              JavaScript 中,理解数据拷贝的深浅至关重要。浅拷贝(如扩展运算符`...`、`Object.assign()`)仅复制对象第一层,共享内部引用,导致修改时产生意外联动。深拷贝(如自定义递归函数、`_.cloneDeep`或`JSON.parse(JSON.stringify())`)创建独立副本,确保数据隔离。选择哪种取决于性能、数据独立性和资源需求。深拷贝虽慢,但确保安全;浅拷贝快,但需小心引用共享。在面试中,理解这些概念及其应用场景是关键。
              49 4
              【JavaScript】JavaScript中的深拷贝与浅拷贝详解:基础概念与区别
              |
              3月前
              |
              JavaScript 前端开发
              JavaScript函数核心概念:用于代码复用与管理。
              【6月更文挑战第25天】JavaScript函数核心概念:用于代码复用与管理。示例包括定义(无参、有参、有返回值)与调用,参数按值传递。函数内修改参数不影响外部变量。
              22 1
              |
              3月前
              |
              JavaScript 前端开发
              深入解析JavaScript中的面向对象编程,包括对象的基本概念、创建对象的方法、继承机制以及面向对象编程的优势
              【6月更文挑战第12天】本文探讨JavaScript中的面向对象编程,解释了对象的基本概念,如属性和方法,以及基于原型的结构。介绍了创建对象的四种方法:字面量、构造函数、Object.create()和ES6的class关键字。还阐述了继承机制,包括原型链和ES6的class继承,并强调了面向对象编程的代码复用和模块化优势。
              40 0
              |
              4月前
              |
              设计模式 JavaScript 前端开发
              在JavaScript中,继承是一个重要的概念
              【5月更文挑战第9天】JavaScript继承有优点和缺点。优点包括代码复用、扩展性和层次结构清晰。缺点涉及深继承导致的复杂性、紧耦合、单一继承限制、隐藏父类方法以及可能的性能问题。在使用时需谨慎,并考虑其他设计模式。
              27 2
              |
              4月前
              |
              JavaScript 前端开发
              JavaScript的概念
              JavaScript的概念
              下一篇
              云函数