zoukankan      html  css  js  c++  java
  • JavaScript系列----作用域链和闭包

    1.作用域链

     1.1.什么是作用域

    谈起作用域链,我们就不得不从作用域开始谈起。因为所谓的作用域链就是由多个作用域组成的。那么, 什么是作用域呢?

     1.1.1作用域是一个函数在执行时期的执行环境。

    每一个函数在执行的时候都有着其特有的执行环境,ECMAScript标准规定,在javascript中只有函数才拥有作用域。换句话,也就是说,JS中不存在块级作用域。比如下面这样:

    function getA() {
      if (false) {
        var a = 1;
      }
      console.log(a);  //undefined
    }
    getA();
    function getB() {
      console.log(b);
    }
    getB();    // ReferenceError: b is not defined

      上面的两段代码,区别在于 :getA()函数中,有变量a的声明,而getB()函数中没有变量b的声明。

      另外还有一点,关于作用域中的声明提前。

    1.1.2.作用域中声明提前

    在上面的getA()函数中,或许你还存在着疑惑,为什么a="undefined"呢,具体原因就是因为作用域中的声明提前:所以getA()函数和下面的写法是等价的:

    function getA(){
       var a;
      if(false){
        a=1
        };
      console.log(a);
    }

    既然提到变量的声明提前,那么只需要搞清楚三个问题即可:

      1.什么是变量

          2.什么是变量声明

          3.声明提前到什么时候。

    什么是变量?

      变量包括两种,普通变量和函数变量。 

    • 普通变量:凡是用var标识的都是普通变量。比如下面 :
      var x=1;               
      var object={};
      var  getA=function(){};  //以上三种均是普通变量,但是这三个等式都具有赋值操作。所以,要分清楚声明和赋值。声明是指 var x; 赋值是指 x=1; 
    • 函数变量:函数变量特指的是下面的这种,fun就是一个函数变量。
      function fun(){} ;// 这是指函数变量. 函数变量一般也说成函数声明。

       
      类似下面这样,不是函数声明,而是函数表达式

      var getA=function(){}      //这是函数表达式
      var getA=function fun(){}; //这也是函数表达式,不存在函数声明。关于函数声明和函数表达式的区别,详情见javascript系列---函数篇第二部分

     

    什么是变量声明?

         变量有普通变量和函数变量,所以变量的声明就有普通变量声明和函数变量声明。

    •  普通变量声明
      var x=1; //声明+赋值
      var object={};   //声明+赋值

       上面的两个变量执行的时候总是这样的

      var x = undefined;      //声明
      var object = undefined; //声明
      x = 1;                  //赋值
      object = {};            //赋值

      关于声明和赋值,请注意,声明是在函数第一行代码执行之前就已经完成,而赋值是在函数执行时期才开始赋值。所以,声明总是存在于赋值之前。而且,普通变量的声明时期总是等于undefined.

    • 函数变量声明
      函数变量声明指的是下面这样的:
      function getA(){}; //函数声明

    声明提前到什么时候?

            所有变量的声明,在函数内部第一行代码开始执行的时候就已经完成。-----声明的顺序见1.2作用域的组成

    1.2.作用域的组成

    函数的作用域,也就是函数的执行环境,所以函数作用域内肯定保存着函数内部声明的所有的变量。

    一个函数在执行时所用到的变量无外乎来源于下面三种:

    1.函数的参数----来源于函数内部的作用域

    2.在函数内部声明的变量(普通变量和函数变量)----也来源于函数内部作用域

    3.来源于函数的外部作用域的变量,放在1.3中讲。

    比如下面这样:

    var x = 1;
    function add(num) () {
      var y = 1; 
      return x + num + y;   //x来源于外部作用域,num来源于参数(参数也属于内部作用域),y来源于内部作用域。
    }

            那么一个函数的作用域到底是什么呢?

       在一个函数被调用的时候,函数的作用域才会存在。此时,在函数还没有开始执行的时候,开始创建函数的作用域:

      函数作用域的创建步骤:

              1. 函数形参的声明。

              2.函数变量的声明

              3.普通变量的声明。  

              4.函数内部的this指针赋值

                 ......函数内部代码开始执行!  

             所以,在这里也解释了,为什么说函数被调用时,声明提前,在创建函数作用域的时候就会先声明各种变量。

       关于变量的声明,这里有几点需要强调

       1.函数形参在声明的时候已经指定其形参的值。  

    function add(num) {
      var num;
      console.log(num);   //1
    }
    add(1);

      2.在第二步函数变量的生命中,函数变量会覆盖以前声明过的同名声明。

    function add(num1, fun2) {
      function fun2() {
        var x = 2;
      }
      console.log(typeof num1); //function  
      console.log(fun2.toString()) //functon fun2(){ var x=2;}
    }
    add(function () {
    }, function () {
      var x = 1
    }); 

    3.  在第三步中,普通变量的声明,不会覆盖以前的同名参数

    function add(fun,num) {
      var fun,num;
      console.log(typeof fun) //function
      console.log(num);      //1
    }
    add(function(){},1);

       在所有的声明结束后,函数才开始执行代码!!! 

     1.3.作用域链的组成

       在JS中,函数的可以允许嵌套的。即,在一个函数的内部声明另一个函数

        类似这样: 

    function A(){
      var  a=1;
       function B(){  //在A函数内部,声明了函数B,这就是所谓的函数嵌套。
             var b=2;   
       }
    }

       对于A来说,A函数在执行的时候,会创建其A函数的作用域, 那么函数B在创建的时候,会引用A的作用域,类似下面这样

      

    函数B在执行的时候,其作用域类似于下面这样:

        从上面的两幅图中可以看出,函数B在执行的时候,是会引用函数A的作用域的。所以,像这种函数作用域的嵌套就组成了所谓的函数作用域链。当在自身作用域内找不到该变量的时候,会沿着作用域链逐步向上查找,若在全局作用域内部仍找不到该变量,则会抛出异常。

    2.什么是闭包 

    闭包的概念:有权访问另一个作用域的函数。

     这句话就告诉我们,第一,闭包是一个函数。第二,闭包是一个能够访问另一个函数作用域。

    那么,类似下面这样,

    function A(){
    
      var a=1;
      
      function B(){  //闭包函数,函数b能够访问函数a的作用域。所以,像类似这么样的函数,我们就称为闭包
      
      }
    }

    所以,创建闭包的方式就是在一个函数的内部,创建另外一个函数。那么,当外部函数被调用的时候,内部函数也就随着创建,这样就形成了闭包。比如下面。

    var fun = undefined;
    function a() {
      var a = 1;
      fun = function () {
      }
    }

     3.闭包所引起的问题

    其实,理解什么是闭包并不难,难的是闭包很容易引起各种各样的问题。

    3.1.变量污染

    看下面的这道例题:

    var funB,
    funC;
    (function() {
      var a = 1;
      funB = function () {
        a = a + 1;
        console.log(a);
      }
      funC = function () {
        a = a + 1;
        console.log(a);
      }
    }());
    funB();  //2
    funC();  //3.

    对于 funB和funC两个闭包函数,无论是哪个函数在运行的时候,都会改变匿名函数中变量a的值,这种情况就会污染了a变量。

    两个函数的在运行的时候作用域如下图:


    这这幅图中,变量a可以被函数funB和funC改变,就相当于外部作用域链上的变量对内部作用域来说都是静态的变量,这样,就很容易造成变量的污染。还有一道最经典的关于闭包的例题:

    var array = [
    ];
    for (var i = 0; i < 10; i++) {
      var fun = function () {
        console.log(i);
      }
      array.push(fun);
    }
    var index = array.length;
    while (index > 0) {
      array[--index]();
    } //输出结果 全是10;

    想这种类似问题产生的根源就在于,没有注意到外部作用域链上的所有变量均是静态的。

    所以,为了解决这种变量的污染问题---而引入的闭包的另外一种使用方式。

     那种它是如何解决这种变量污染的呢?  思想就是: 既然外部作用域链上的变量时静态的,那么将外部作用域链上的变量拷贝到内部作用域不就可以啦!! 具体怎么拷贝,当然是通过函数传参的形式啊。

    以第一道例题为例:

    var funB,funC;
    (function () {
      var a = 1;
      (function () {
        funB = function () {
          a = a + 1;
          console.log(a);
        }
      }(a));
      (function (a) {
        funC = function () {
          a = a + 1;
          console.log(a);
        }
      }(a));
    }());
    funB()||funC();  //输出结果全是2 另外也没有改变作用域链上a的值。

      在函数执行时,内存的结构如图所示:

    由图中内存结构示意图可见,为了解决闭包的这种变量污染的问题,而加了一层函数嵌套(通过匿名函数自执行),这种方式延长了闭包函数的作用域链。

     

    3.2.内存泄露

    内存泄露其实严格来说,就是内存溢出了,所谓的内存溢出,当时就是内存空间不够用了啊。

    那么,闭包为什么会引起内存泄露呢?

    var fun = undefined;
    function A() {
      var a = 1;
      fun = function () {
      }
    }

    看上面的例题,只要函数fun存在,那么函数A中的变量a就会一直存在。也就是说,函数A的作用域一直得不到释放,函数A的作用域链也不能得到释放。如果,作用域链上没有很多的变量,这种牺牲还可有可无,但是如果牵扯到DOM操作呢?

    var element = document.getElementById('myButton');
    (function () {
      var myDiv = document.getElementById('myDiv')
      element.onclick = function () {
        //处理程序
      }
    }())

    像这样,变量myDiv如果是一个占用内存很大的DOM....如果持续这么下去,内存空间岂不是一直得不到释放。久而久之,变引起了内存泄露(也是就内存空间不足)。

        

     

     

     

  • 相关阅读:
    Linux进程管理工具Supervisor
    RSA加密传输代码示例
    静态网站创建工具Docusaurus
    Proactor和Reactor模型
    机器学习中的七宗罪
    Tokio internals: Understanding Rust's asynchronous I/O framework from the bottom up
    开源软件创建SOC的一份清单
    How to setup SOC using open-source tools
    彼得定律
    深入浅出通信原理连载
  • 原文地址:https://www.cnblogs.com/renlong0602/p/4398883.html
Copyright © 2011-2022 走看看