zoukankan      html  css  js  c++  java
  • 【javascript】Javascript闭包

      在描述闭包的实现与用途前,需要了解以下一些知识点。



      执行上下文(执行上下文环境) 


    1 console.log(a);     //Uncaught ReferenceError: a is not defined 
    2 
    3 console.log(b);    //undefined
    4 var b;
    5 
    6 console.log(c);    //undefined
    7 var c = 10;

      看上面一个例子,当直接在控制台输出a时,提示a未定义,这个很容易就理解了。不过在"var b"和"var c = 10"执行前输出b和c的值时,得到的结果却是undefined。因为代码是一条一条执行的,在执行console.log()之前,"var b"和"var c = 10"必定是还未执行的。而结果却与a的输出结果不同,这是怎么一回事?

     第二种情况:

     1 console.log(this); 

    输出:

    第三种情况:

    1 console.log(f1);  
    2 function f1(){};  //函数声明
    3 
    4 console.log(f2);
    5 var f2 = function (){};  //函数表达式

    输出: 

      从这里可以看出来,在一段js代码拿过来真正一句一句运行之前,浏览器做了一些“准备工作”,其中就包括对变量的声明,而不是赋值,变量赋值是在赋值语句执行的时候进行的,还有函数的声明的赋值。而函数表达式类似与第一种情况中的var c = 10,两者结果也是相同的。当然还有this的赋值等等。

      下面总结一下浏览器在准备中做了哪些工作:

        变量、函数表达式 —— 变量声明,默认赋值为undefined;

        this —— 赋值 

        函数声明 —— 赋值

      这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

      以上的一切都是在全局环境中进行的。

      在函数中,除了以上数据之外,还会有其他数据。

    1 function f(x){
    2     console.log(arguments);
    3     console.log(x);
    4 }
    5 f(10);

    输出:

      以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。

      从而我们可以看出来,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

      进一步来说,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。

    总结一下:

      全局代码的上下文环境数据内容为:

    普通变量(包括函数表达式),

    如: var a = 10;

    声明(默认赋值为undefined)

    函数声明,

    如: function fn() { }

    赋值

    this

    赋值

      如果代码段是函数体,那么在此基础上需要附加:

    参数

    赋值

    arguments

    赋值

    自由变量的取值作用域

    赋值

      给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。



      在执行js代码时,会有数不清的函数调用次数,会产生许多个上下文环境。这么多上下文环境该如何管理,以及如何销毁而释放内存呢?这就引入了一个概念——执行上下文栈


      在这里插播一个概念 —— this

      在平时,this使用频率非常高,我们在很多情况下都会用到它。而无论在哪个位置获取this,都是有值的。在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。

      接下来,分四种情况描述this的取值:

      情况1:构造函数

        所谓构造函数就是用来new对象的函数。其实严格来说,所有的函数都可以new一个对象,但是有些函数的定义是为了new一个对象,而有些函数则不是。另外注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function等。

        如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。这里有一个情况,当函数的调用不是new一个对象,而是直接调用函数时,其中的this的值为window。

        这里还有一个问题,在构造函数的prototype中,this代表着什么。其实不仅仅是构造函数的prototype,即便是在整个原型链中,this代表的也都是当前对象的值。

      情况2:函数作为对象的一个属性

        如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。

      情况3:函数用call或者apply调用

        当一个函数被call和apply调用时,this的值就取传入的对象的值。 

      情况4:全局 & 调用普通函数

        在全局环境下,this永远是window,普通函数在调用时,其中的this也都是window。


     下面继续学习上下文栈。

      执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。这其实就是一个压栈出栈的过程——执行上下文栈。如图:

      在上下文栈中,同一时间内只有一个活跃的上下文环境。

      当一个函数被调用时,将产生一个新的执行上下文环境,并被压入执行上下文栈中,原先的栈顶执行上下文环境进入非活跃状态,新压入的执行上下文环境则进入活动状态。

      当函数调用完成后,栈顶执行上下文环境被弹出栈,并被回收机制回收销毁,而新的栈顶上下文环境则进入活跃状态。

      不过这只是一个理想的运行情况,有时候执行上下文环境并不会说销毁就会被销毁的,这就是后面要介绍的——闭包。



       作用域 & 作用域链


      无论在哪个编程语言中,都有一个作用域的概念。而在javascript中,除了全局作用域之外,只有函数可以创建的作用域。作用域在函数定义时就已经确定了。而不是在函数调用时确定。

      所以,我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。因为在函数中,如果声明变量时没用"var",则所声明的变量为全局变量。

      作用域是一个很抽象的概念,类似于一个“地盘”,如下图:

      作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。

      作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

      我们把作用域与上下文环境结合起来看,如下图:

      

      在上图中,我们可以看到,作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

      所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。

      这里有一个概念——自由变量。所谓"自由变量",即在这一个作用域中调用的变量,并没有在这个作用域中声明,而是在其他作用域声明的变量,即称之为"自由变量"。

      而我们在这个作用域中调用自由变量时,究竟要到哪里去获取这个变量的值呢?有人会说,到这个作用域的父作用域调用。这个说法有点模糊,有时候会产生歧义。正确来说,应该是:要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”。其实这也就是所谓的"静态作用域"。

      上面描述的只是跨一步作用域去寻找。

      如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

      这个一步一步“跨”的路线,我们称之为——作用域链。

      下面我们来用文字描述一下这个过程:(假设a为自由变量)  

        第一步,现在当前作用域查找a,如果有则获取并结束。如果没有则继续;

        第二步,如果当前作用域是全局作用域,则证明a未定义,结束;否则继续;

        第三步,(不是全局作用域,那就是函数作用域)将创建该函数的作用域作为当前作用域;

        第四步,跳转到第一步。



      接下来就是主菜 —— 闭包


        关于闭包,百度的解释是:闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。。。看起来有点云里雾里的。

      其实,我们只要了解应用的两种情况即可——函数作为返回值,函数作为参数传递。

      函数作为返回值:

     1 function fn(){
     2     var a = 10;
     3     
     4     function bar(x){
     5         if (X < a){
     6             console.log(X);
     7         }
     8     }
     9     
    10     return bar;
    11 }

      函数作为参数被传递:

     1 var 
     2     max = 10;
     3     fn = function(X){
     4          if (X > max){
     5              console.log(X);
     6          }
     7          else{
     8              console.log(max);
     9          }
    10      }; 
    11 
    12 (function(f) {      
    13      var max = 100;
    14      f(15);
    15  })(fn);

      这里的输出结果为: 15。原因为取max的值时取的是创建这个函数的那个作用域中获取,而不是“父作用域”。所以取的值为10,而不是100。

      结合前面学过的,我们可以看看下面这张图:

      在图中我们可以看到,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。

      执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。

      执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。而重点在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。从而fn上下文环境依旧在栈中没有销毁。

      虽说闭包的使用增加了内存的消耗,而且使用不当有可能造成内存泄露的问题发生,不过其还是有很多的用途。譬如匿名自执行函数、匿名自执行函数、实现封装以及实现面向对象中的对象,模拟传统的对象语言都提供类的模板机制等等。

  • 相关阅读:
    mongodb集群搭建
    ansible 部署redis集群(三台主机6节点3主3备)
    nginx upstream bug
    回炉重造之 nginx
    docker 搭建jenkins环境
    搭建gitlab
    Dockerfile传入变量问题
    mysql8忘记root密码重置密码
    docker-compose容器化构建nginx服务
    docker-compose容器化构建minio服务
  • 原文地址:https://www.cnblogs.com/linxiong945/p/4001436.html
Copyright © 2011-2022 走看看