zoukankan      html  css  js  c++  java
  • JavaScript:作用域与作用域链

    1.什么是作用域(scope)?

    简单来讲,作用域(scope)就是变量访问规则的有效范围

    • 作用域外,无法引用作用域内的变量;
    • 离开作用域后,作用域的变量的内存空间会被清除,比如执行完函数或者关闭浏览器
    • 作用域与执行上下文是完全不同的两个概念。我曾经也混淆过他们,但是一定要仔细区分。

    JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

    函数作用域是在函数声明的时候就已经确定了,而函数执行上下文是在函数调用时创建的。假如一个函数被调用多次,那么它就会创建多个函数执行上下文,但是函数作用域显然不会跟着函数被调用的次数而发生什么变化。

    1.1 全局作用域

    var foo = 'foo';
    console.log(window.foo);   // => 'foo' 

    在浏览器环境中声明变量,该变量会默认成为window对象下的属性。

    function foo() {
        name = "bar"
    }
    foo();
    console.log(window.name) // bar

    在函数中,如果不加 var 声明一个变量,那么这个变量会默认被声明为全局变量,如果是严格模式,则会报错。

    全局变量会造成命名污染,如果在多处对同一个全局变量进行操作,那么久会覆盖全局变量的定义。同时全局变量数量过多,非常不方便管理。

    这也是为什么jquery要在全局建立变量 ,其余私有方法属性挂在 下的原因。

    1.2 函数作用域

    假如在函数中定义一个局部变量,那么该变量只可以在该函数作用域中被访问。

    function doSomething () {
        var thing = '吃早餐';
    }
    console.log(thing); // Uncaught ReferenceError: thing is not defined

    嵌套函数作用域:

    function outer () {
        var thing = '吃早餐';
        function inner () {
            console.log(thing);
        }
        inner();
    }
    
    outer();  // 吃早餐

    在外层函数中,嵌套一个内层函数,那么这个内层函数可以向上访问到外层函数中的变量。

    既然内层函数可以访问到外层函数的变量,那如果把内层函数return出来会怎样?

    function outer () {
        var thing = '吃早餐';
        
        function inner () {
            console.log(thing);
        }
        
        return inner;
    }
    
    var foo = outer();
    foo();  // 吃早餐

    函数执行完后,函数作用域的变量就会被垃圾回收。而这段代码看出当返回了一个访问了外部函数变量的内部函数,最后外部函数的变量得以保存。

    这种当变量存在的函数已经执行结束,但扔可以再次被访问到的方式就是“闭包”。后期会继续对闭包进行梳理。

    1.3 块级作用域

    很多书上都有一句话,javascript没有块级作用域的概念。所谓块级作用域,就是{}包裹的区域。但是在ES6出来以后,这句话并不那么正确了。因为可以用 let 或者 const 声明一个块级作用域的变量或常量。

    比如:

    for (let i = 0; i < 10; i++) {
        // ...
    }
    console.log(i); // Uncaught ReferenceError: i is not defined

    发现这个例子就会和函数作用域中的第一个例子一样的错误提示。因为变量i只可以在 for循环的{ }块级作用域中被访问了。

    扩散思考:

    究竟什么时候该用let?什么时候该用const?

    默认使用 const,只有当确实需要改变变量的值的时候才使用let。因为大部分的变量的值在初始化之后不应再改变,而预料之外的变量的修改是很多bug的源头。

    1.4 词法作用域

    词法作用域,也可以叫做静态作用域。意思是无论函数在哪里调用,词法作用域都只在由函数被声明时所处的位置决定。
    既然有静态作用域,那么也有动态作用域。
    而动态作用域的作用域则是由函数被调用时执行的位置所决定。

    var a = 123;
    function fn1 () {
        console.log(a);
    }
    function fn2 () {
        var a = 456;
        fn1();
    }
    fn2();   // 123

    以上代码,最后输出结果 a 的值,来自于 fn1 声明时所在位置访问到的 a 值 123。
    所以JS的作用域是静态作用域,也叫词法作用域。

    上面的1.1-1.3可以看做作用域的类型。而这一小节,其实跟上面三小节还是有差别的,并不属于作用域的类型,只是关于作用域的一个补充说明吧。

    2. 什么是作用域链(scope chain)

    在JS引擎中,通过标识符查找标识符的值,会从当前作用域向上查找,直到作用域找到第一个匹配的标识符位置。就是JS的作用域链。

    var a = 1;
    function fn1 () {
        var a = 2;
        function fn2 () {
            var a = 3;
            console.log(a);
        }
        fn2 ();
    }
    fn1(); // 3

    console.log(a) 语句中,JS在查找 a变量标识符的值的时候,会从 fn2 内部向外部函数查找变量声明,它发现fn2内部就已经有了a变量,那么它就不会继续查找了。那么最终结果也就会打印3了。

    代码分析如下:

    <script type="text/javascript">
        var a = 100;
        function fun(){
            var b = 200
            function fun2(){
                var c = 300
            }
            function fun3(){
                var d = 400
            }
            fun2()
            fun3()
        }
        fun()
    </script>

    首先预编译,一开始生成一个GO{

      a:underfined

      fun:function fun(){//fun的函数体

          var b = 200
          function fun2(){
            var c = 300
          }
          function fun3(){
          var d = 400
          }
          fun2()
          fun3()
        }

    }

    逐行执行代码,GO{

      a:100

      fun:function fun(){//fun的函数体

          var b = 200
          function fun2(){
            var c = 300
          }
          function fun3(){
          var d = 400
          }
          fun2()
          fun3()
        }

    }

    当fun函数执行时,首先预编译会产生一个AO{

      b:underfined

      fun2:function fun2(){
           var c = 300
         }

      fun3:function fun3(){
          var d = 400
         }

    }

    这里注意的是fun函数是在全局的环境下产生的,所以自己身上挂载这一个GO,由于作用域链是栈式结构,先产生的先进去,最后出来,

    在这个例子的情况下,AO是后于GO产生的,所以对于fun函数本身来说,执行代码的时候,会先去自己本身的AO里找找看,如果没有找到要用的东西,就去父级查找,此题的父级是GO

    此刻fun的作用域链是  第0位    fun的AO{}

              第1位    GO{}

    fun函数开始逐行执行AO{

      b:200

      fun2:function fun2(){
           var c = 300
         }

      fun3:function fun3(){
          var d = 400
         }

     }

    注意:函数每次调用才会产生AO,每次产生的AO还都是不一样的

    然后遇到fun2函数的执行,预编译产生自己的AO{

      c:underfined

    }

    此刻fun2的作用域链是第0位    fun2的AO{}

              第1位    fun的AO{}

              第2位    GO{}

    然后遇到fun3函数的执行,预编译产生自己的AO{

      d:underfined

    }

    此刻fun3的作用域链是第0位    fun3的AO{}

              第1位    fun的AO{}

              第2位    GO{}

    fun2和fun3的作用域链没有什么联系。

    当函数fun2和fun3执行完毕,自己将砍掉自己和自己的AO的联系,

    最后就是fun函数执行完毕,它也是砍掉自己和自己AO的联系。

    这就是一个我们平时看到不是闭包的函数。

    闭包

    1.闭包在红宝书中的解释就是:有权访问另一个函数作用域中的变量的函数。

    2.写法:

     1 <script type="text/javascript">
     2     function fun1(){
     3         var a = 100;
     4         function fun2(){
     5             a++;
     6             console.log(a);
     7         }
     8         return fun2;
     9     }
    10     
    11     var fun = fun1();
    12     fun()
    13     fun()
    14 </script>

    3.效果如下:

    4.分析:

    执行代码

    GO{

    fun:underfined

    fun1:function fun1()

       {

         var a = 100;

         function fun2()

        {

            a++;

            console.log(a);

         }

         return fun2;

         }

    }

    然后第十一行开始这里,就是fun1函数执行,然后把fun1的return返回值赋给fun,这里比较复杂,我们分开来看,

    这里fun1函数执行,产生AO{

    a:100

    fun2:function fun2(){

        a++;
        console.log(a);
        }

    }

    此刻fun1的作用域链为 第0位   AO

               第1位   GO

    此刻fun2的作用域链为 第0位   fun1的AO

               第1位   GO

    解释一下,fun2只是声明了,并没有产生调用,所以没有产生自己的AO,

    正常的,我们到第7行代码我们就结束了,但是这个时候来了一个return fun2,把fun2这个函数体抛给了全局变量fun,好了,fun1函数执行完毕,消除自己的AO,

    此刻fun2的作用域链为 第0位   fun1的AO

               第1位   GO

    第十二行就是fun执行,然后,它本身是没有a的,但是它可以用fun1的AO,然后加,然后打印,

    因为fun中的fun1的AO本来是应该在fun1销毁时,去掉,但是被抛给fun,所以现在fun1的AO没办法销毁,所以现在a变量相当于一个只能被fun访问的全局变量。

    所以第十三行再调用一次fun函数,a被打印的值为102。

  • 相关阅读:
    C#中判断为空
    ArcGIS中的AddIn开发示例
    当前不会命中断点,还没有加载该文档加载任何符号
    设置ArcGIS的外观改回到出厂
    读取Style符号库样式的方法
    ArcEngine中的缩放地图
    修改字段结构之GP工具
    修改字段结构之ArcGIS Diagrammer
    merage语句
    Windows下Redis的安装使用
  • 原文地址:https://www.cnblogs.com/xuxiaoyu/p/10137129.html
Copyright © 2011-2022 走看看