JavaScript-作用域 和 闭包

  • 作用域定义和作用

      作用域定义:JavaScript中存储变量和查找变量的一套规则。

       JavaScript中一般遵循词法作用域。    

      变量查找遵循从内到外查找:  

      引擎从当前的执行作用域开始查找变量,如果找不到就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是找不到,查找过程都会停止。

  • 词法作用域

         定义在词法阶段的作用域。是在写代码的时将变量和块作用域写在哪里来来决定的。

  •  作用域的产生

          JavaScript中没有块状作用域的概念。那么什么是块状作用域呢 简单说就是{..}括起来的区域就是一个最小单元的块。

 我们可以简单 回顾一下c#或者Java语言中变量的作用域和申明周期 一般情况下是这样的

     C#或者java中变量的作用域:

    作用域从变量定义的位置开始,到该变量所在的那对大括号结束;

生命周期:

    变量从定义的位置开始就在内存中活了;

    变量到达它所在的作用域的时候就在内存中消失了;

 

举一个最简单的例子 for循环 

C#中 for中定义的i 变量在 循环外访问不到

 

 

          JavaScript 中在for循环外 可以正常访问

 

 

 

   那么JavaScript 在什么时候创建作用域呢

  1.   全局作用域  

           这个无需做过多的解释,相信大家都理解

          2. 函数

           javaScript中每创建一个函数都会为其自身创建一个作用域气泡。属于这个函数的所有变量可以在整个函数内部使用及复用

JavaScript 的作用域是基于函数的吧,变量的作用域为:

      使用关键字var声明一个变量,那么这个变量就属于当前函数的作用域,如果声明是发生在任何函数外的顶层声明,那么这个变量则属于全局作用域。

      没有使用var 关键字声明的变量会默认添加到全局作用域中,要尽量避免这样写。

  • 块状作用域 

      上面说了js中没有块状作用域,但是凡事都有例外,js中有几种情况可以提供块状作用域的效果

1: Try/catch   

Catch 分句会创建一个块状作用域,其中声明的变量仅在catch内部有效  

        <script type="text/javascript">
            try{
                undefine();
            }catch(err)
            {
                console.log(err);   //可以访问
                var a=10;
                console.log(a);
            }
            console.log("a"+a);  //可以访问  
            console.log(err);   //不可访问
        </script>

   

 

    以上还是要补充说明一下: 这个块状作用域是指 catch()后的这个括号的什么的变量。 在catch(){}花括号里面申明的变量不属于块状作用域。

  2: Let

Let 可以将变量的作用域绑定在所在的 {…}块状作用域中

Var foo=true;

If(foo){

  Let bar=foo*2;

  Console.log(bar);  //正常访问

}

Console.log(bar); // ReferenceError

l  使用 let 进行的声明不会再块状作用域中进行提升。

{

  Console.log(bar);// ReferenceError

 Let bar=2;

}

              3:Const

Const 也会创建块状作用域,但是它的值是固定的,之后任何试图修改的操作都会引起错误。

4: 立即执行函数

 

<script type="text/javascript">

    {
        (function(){
            var j=5;  //立即执行函数定义的变量属于作用快
          console.log(j);
        })();
        
        console.log(j); //这里访问不到
    }
    </script>
    

 

 

  • 作用域提升

             定义:  无论var出现在一个作用域中的哪个位置,这个声明都属于整个作用域,在其中到处都可以访问的,这一行为被比喻为提升。

      特点:函数声明和变量声明会提升。而赋值或其他运行逻辑会留在原地。当函数声明和变量声明同时出现时,函数会首先被提升,然后才是变量。

                       函数表达式不会被提升

  作用域提升会被提升到所在块状作用域的的顶部,即所在{…}的顶部 

<script type="text/javascript">
    foo(); //访问不到
  //  var a=true;
            {
               foo(); //可以访问得到
               function foo(){
                   console.log("a");
               }
           }
       </script> 
  • 作用域闭包  

     前提背景:函数一般在执行后,整个函数内部作用域就会被销毁。

     定义: 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域以外执行,闭包可以使函数继续访问定义时的词法作用域。

     产生的场景:  函数作为返回值赋值给一个变量。(回调函数)

示例一:

<script type="text/javascript">
    function foo() {
        var a = 2;

        function bar() {
            console.log(a);
        }
        return bar;
    }

    var baz = foo();
    baz(); // foo()作用域并美誉在执行后被销毁,a还可以被正常访问,这就是闭包的效果
</script>

 示例二:

    <script type="text/javascript">
        function foo()
        {
            var a=2;
            function baz()
            {
              console.log(a);
            }
            bar(baz);
        }
        
        function bar(fn){
            fn();   //这就是闭包的效果
        }
        foo();
        
    </script>

     无论通过何种手段将内部函数传递到所在词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行整个函数都会使用闭包。

 常见闭包场景

场景一:这个示例比较简单

<script type="text/javascript">
      function wait(message){
           setTimeout(function timer(){
               console.log(message);
           },1000);
      }
        
        wait("Hello,closure!"); // timer();函数被添加到任务队列中。 函数保留了词法作用域对wait();函数作用域的引用,所有可以访问到message变量
    </script>

场景二:在循环中 很多同学都会有点迷惑

<script type="text/javascript">
    for(var i=1;i<=5;i++)
    {
        setTimeout(function timer(){
            console.log(i);
        },i*1000);
    }
    </script>

上面的代码会输出什么,正常情况下我我们对这段代码的期待是分别输出数组1~5,每秒一个,每次一个。但是实际上,这段代码在运行时会以每秒一次的频率输出5次6;

    

 为什么会这样呢? timer()函数会在for循环结束后执行,fou循环结束的条件是i<=5;即i=6;  timer();保留了for循环的作用域引用,此时i=6; 所有会输出5个6;

如果要实现我们的预期效果就需要为每一次的循环创建作用域闭包(块状作用域)

方式一:let

    <script type="text/javascript">
    for(var i=1;i<=5;i++)
    {
        let j=i;  //使用let 创建块状作用域
        setTimeout(function(){
            console.log(j);
        },i*1000);
    }
    或者
      for(let a=5;a>=1;a--)  // //使用let 创建块状作用域
    {
        
        setTimeout(function(){
            console.log(a);
        },a*1000);
    }
    
    
    </script>

方式二:立即执行函数

  for(var i=1;i<=5;i++)
    {
        (function(){
            var j=i;
        setTimeout(function(){
            console.log(j);
        },j*1000);
        })();
    }
    </script>
  • 立即执行函数

产生和意义:

在任意代码判断外部包装一个函数,可以达到封装的的目的,函数内部的变量和函数定义不会被外部作用域访问。但是必须声明一个具名函数,这个函数的名称也会污染所在的作用域。而且必须要显示的调用这个函数才能运行其中的代码。

如果函数不需要指定函数名称,并且能够自动执行。这时候立即执行函数就诞生了

语法:把函数包含在()中可以形成还是表达式,通过在未尾加上() 可以立即执行这个函数。(function fn(){})();

  • 函数表达式

    定义:函数表达式区别于函数声明,也是一种定义函数的方式,形似与变量赋值,这个值就是函数体,例

    var a = function(){}; // 函数表达式之匿名函数

    var a = function fn(){}; // 函数表达式之具名函数

    (function(){})(); // 匿名函数之立即执行函数

    特点:

    1 . 区别于函数声明,和普通变量一样使用前必须声明,不声明在非严格模式下被认为是全局的变量,在严格模式下报错

 

原文地址:https://www.cnblogs.com/cuner/p/12453565.html