阿西河

所有教程

公众号
🌙
阿西河前端的公众号

我的收藏

    最近访问  (文章)

      教程列表

      抓包专区
      测试专区

      JavaScript 作用域和闭包

      作用域和闭包

      ECMAScript5;js的代码没有代码块;使用函数运行的机制进行创建闭包;闭包就是作用域的意思;ES5中,JS中只有函数才可以创建能操作的作用域;

      JavaScript中的内存也分为栈内存和堆内存。一般来说,栈内存中存放的是存储对象的地址,而堆内存中存放的是存储对象的具体内容。对于原始类型的值而言,其地址和具体内容都存在与栈内存中;而基于引用类型的值,其地址存在栈内存,其具体内容存在堆内存中。堆内存与栈内存是有区别的,栈内存运行效率比堆内存高,空间相对推内存来说较小,反之则是堆内存的特点。所以将构造简单的原始类型值放在栈内存中,将构造复杂的引用类型值放在堆中而不影响栈的效率。

      • 函数执行时形成私有作用域
      • 全局变量和私有变量
      • 如何区分函数中出现的变量是私有的还是非私有的
      • 如何查找当前作用域的上级作用域?
      • JavaScript中的顶级作用域是谁?
      • 变量和属性有什么关系
      • 函数运行产生的作用域
      • 闭包
      • 作用域不销毁的情况|内存释放

      函数执行时形成私有作用域

      函数执行的时候(直接目的:让函数体中的代码执行)会形成一个新的私有的作用域(栈内存),供函数体中的代码执行;

      • 1)给形参赋值
      • 2)私有作用域下的预解释
      • 3)私有作用域下的代码执行

      形成的新的私有的作用域还保护了里面的私有变量不受外界的影响,我们把函数的这种保护机制叫"闭包":为什么要有作用域;因为变量要规定活动范围的,为便于管理不同范围的变量;所以要给变量设置活动范围;

      预解释也是在各自的作用域里进行预解释的;

      function fn(){
          var a=1;
      }
      fn();
      fn();
      console.log(a);//Uncaught ReferenceError: a is not defined;
      

      因为a没有声明和定义过,所以报错了;

      上面的fn运行了两次,所以产生了两个堆内存;两个作用域(作用域也就是闭包)各自分别有一个a的变量;a的值都是数字1,但是两个变量是不相等的;两个a之间是没有任何关系的;

      就好比我们都属于人类;我们都继承了人类这个对象所具有的特征;我有一双手,你也有一双手,但是我们两个人的手是没有关系的;我的手不等于你的手;

      全局变量和私有变量·

      • 在全局作用域下声明的变量是全局变量;
      • 在私有作用域中声明的变量是私有变量;函数的形参也是私有的变量;

      如何区分函数中出现的变量是私有的还是非私有的?

      首先看是否为形参,然后看是否在私有作用域中声明过(有没有var过),两者有其一就是私有的变量,那么在当前函数中不管什么位置出现都是私有的,和全局的没有半毛钱的关系;如果两者都没有,说明不是私有的;如果一个函数中出现的变量不是私有的,那么会往其上级作用域查找,上级没有则继续查找,一直找到window为止,如果window也没有呢?

      如果是获取:会报错

      function fn() {
          console.log(num);//Uncaught ReferenceError: num is not defined
      }
      fn();
      

      如果是设置:不是私有的,找全局,全局没有的话相当于给全局加一个

      function fn() {
          num = 13;//相当于给window增加了一个叫做num的属性名,属性值是13 window.num=13;
      }
      fn();
      console.log(num);//13
      

      如何查找当前作用域的上级作用域?

      当前作用域的上级作用域是谁和函数在哪执行的没有任何的关系,我们只需要看当前函数是在哪个作用域下定义的,那么它的上级作用域就是谁;

      下面是查找上一级作用域,一直找到window的案例;

      var total = 100;
      function fn() {
          var total = 10;
          return function () {
              console.log(total);
          }//如果返回的是一个引用数据类型的值(对象、数组、函数...),首先是把这个值开辟一个内存空间,有一个地址,然后把内存地址返回 ->例如在这里其实返回的就是 return xxxfff111;
      }
      var f = fn();//f=xxxfff111;
      f();
      

      //输出的结果是 10 还是 100 ? 为什么 ?

      如果是在自执行函数里呢?

      var total = 100;
      function fn() {
          var total = 10;
          return function () {
              console.log(total);
          }
      }
      var f = fn();//f=xxxfff111;
      ~function () {
        var total = 1200;
        f();//->;里面变量的上级作用域是谁
      }();
      

      下面代码输出的是什么?

      var a=0;
      var b="0";
      function fn(){
          console.log(a);
        console.log(b);
        console.log(typeof a);
          console.log(typeof b);
        a="1";
        var b=1;
        console.log(a);
        console.log(b);
        console.log(typeof a);
        console.log(typeof b);
      }
      fn();//运行后会输出什么?如果没有这行代码;函数fn的定义是没有意义的;函数只声明定义,而不运行是没有任何意义的;
      //0,undefined,"number","undefined","1",1,"string","number"
      

      预解释是作用域中的预解释;js里是可以函数里面套函数的;都运行的时候,是在函数创建的作用域里再创建一个作用域;

      下面是作用域的范围;

      var a="window";
      console.log(a);//window
      function father(){
        console.log(a);//undefined
        var a="father";
        console.log(a);//father
        function children(){
            console.log(b);//undefined
            console.log(a);//father
            var b="flag";
            a="children";
            console.log(a);//children
         }
         children();
         console.log(a);//children
      }
      father();
      console.log(a);//window
      

       JavaScript中的代码执行顺序是从上到下逐条运行的;遇到function定义函数的代码块;直接跳过;遇到函数执行的代码;就找到引用的函数地址;开始跳到执行函数产生的作用域中执行代码;等函数执行完以后,再回到当前作用域执行下面的代码;

       上面的代码运行后,输出的是

      “window”,undefined,“father”,undefined,“father”,“children”,“children”,“window”

      作用域链查找:当作用域套作用域的时候,children内找不到某个变量;会到children的父作用域father中找;当father中找不到的时候;会到father的父作用域找;一直找到window这个根作用域;属于作用域链式查找;

      JavaScript中不带var声明变量,变量会冒泡到上一级作用域中;也就是说会污染到上一级作用域;但是对上上级的作用域是没有影响的;很多教材和视频上说不带var声明的变量;变量会变成全局变量,这个说法是不全面的;

      【带var和不带var的区别和关系】

      只有在全局作用域下才有这样的关系

      区别:带var的可以在代码执行前进行声明,而不带var的不能提前的声明;例子如下;

      //预解释:var num1;
      console.log(num1);//->undefined
      var num1 = 12;
      console.log(num2);//->Error:num2 is not defined
      num2 = 12;
      

      关系:在全局作用域下,var num1=12,不仅仅是增加了一个全局变量,而且还相当于给window增加了一个叫做num1的属性名 -> window.num1=12;

      var num1 = 12;
      console.log(num1);//12
      console.log(window.num1);//12
      
      num2 = 13;
      console.log(num2);
      console.log(window.num2); //我们可以把window.省略;
      

      下面是函数内带var和不带var的;

      console.log(total); //undefined
      var total = 0;
      function fn() {
        console.log(total); //undefined
       total += 100;
        var total = 200;//total是被保护的;
       console.log(total); //200
      }
      fn();
      console.log(total); //0
      

      下面是不带var的

      console.log(total);//undefined
      var total = 0;
      function fn(num1, num2) {
          console.log(total);//0 ->total不是私有的,找全局下的total,也就是在这里出现的所有的total其实应该都是全局的
          total = num1 + num2;//->全局的total=300
          console.log(total);//300
      }
      fn(100, 200);
      console.log(total);//300
      

      JavaScript中的顶级作用域是谁?

      顶级作用域是Global;数据引用高程三133页;

      目前只有nodeJs这个语言可以访问到global;nodeJs中是进程;叫process;后台编程中是不管浏览器的事的,node中是global;JavaScript中只可以访问到window;

      window相当于global的代理人;好比上帝和教皇的关系;一般在JS中说的全局变量都是window;全局变量就是window的属性;通过window.varName可以访问到;

      但并不是所有的都属于window的,比如Object,Math,String,Function等这6个包装类,就不属于window的属性;和window是无关的;类似于中国党政军的关系;互相独立的;

      window只是前端编程里的顶级作用域;

      变量和属性有什么关系

      全局变量是window的属性;window的属性也可以看做全局变量;一个对象的属性,也可以看做以这个对象为作用域下的变量with语句;

        如下:

        <div id="div1"></div>
        <script>
        var oDiv1=document.getElementById("div1");
        oDiv1.style.width="100px";
       oDiv1.style.height="100px";
       oDiv1.style.backgroundColor="red";
      
       //上面写的好麻烦!每次都要写oDiv1.style;可以用with来写
      with(oDiv1.style){
        width="200px";
        height="200px";
        backgroundColor="yellow";
      }
      

        上面就是以oDiv1.style为作用域操作width,height,backgroundColor这些属性;

      提示:background-color在CSS里用。backgroundColor在js处理CSS里用,是DOM.style.backgroundColor,要注意大小写(字母C)。在JS中指定该样式时,这两个都能起作用,但请在JS中尽量使用backgroundColor;上面用background也是有效果的,但是不推荐那么做;

      在JS中不推荐用with语句;不利于团队合作;因为容易引起作用域的混乱;

      如下

      var height="flag",
          a=0;
      with(oDiv1.style){
          width="200px";
          height="200px";
          backgroundColor="yellow";
          console.log(height);//"200px";这样只能访问到本作用域的height;如果想访问全局的height;需要用window.height;访问别的作用域要视情况而定
          console.log(window.height)//"flag";这时候访问的是全局作用域;
          var a=1;//用var来定义变量;
          console.log(a);//1
      }
      console.log(a);//1;此时with里面定义变量,即时用var来定义;也会冒泡到全局;成为全局对象;
      

      如果这个时候with在一个函数里面的;那就更热闹了;下面是执行后的输出;

      var oDiv1=document.getElementById("div1");
      var a=0;
      var height="window";
      function fn(){
          console.log(a);//undefined;
          console.log(height);//undefined;
          var a=1;
          height="fn";
          with(oDiv1.style){
              height="200px";
            console.log(height);//200px;
            console.log(window.height);//window;
            var a=2;
            var height="300px";
            console.log(a);//2
            console.log(height);//"300px"
          }
         console.log(a);//2;此时with里面定义变量,即时用var来定义;也会冒泡到上一级作用域;
         console.log(height);//fn
      }
      fn();
      console.log(a);//0;
      

      在函数里面写with;不仅能把同事坑哭;自己也能被坑的够呛;如果函数里面的with再调用别的作用域内的变量,估计同事就直接掀桌了;所以with还是不要用了,强烈不推荐用with;

      下面函数fn运行后,里面的a是谁的属性?

      function fn(){var a=9} fn();

      此时的a是fn运行所产生的那个作用域的属性;属于fn产生的那个作用域的;产生的那个作用域是活动的,有生命周期的;当浏览器空闲的时候,会把没有用的作用域给释放掉;

      函数运行产生的作用域

      函数的运行是一个有生命周期的内存地址;

      函数运行时,会创建一个内存地址(产生堆内存,函数保存的就是这个堆内存的地址),当此函数运行结束时,此内存地址又会销毁;这个地址,我们无法保存;它的灵活的,活动的;有生命周期的;我们也没有办法给这个作用域起一个变量名字,也没办法保存这个作用域,JS不提供这种机制;

      也就是说:在作用域外面是没办法控制作用域内部的数据的;只能在作用域内部控制;而且作用域内部的代码可以控制外部的数据;这种机制就叫做闭包,闭包与作用域链和函数的运行有关系的;

      函数里的变量,就在这个内存里创建;我们可以把这个内存当成一个对象;那函数里的变量就是这个内存对象的属性;

      函数的定义和函数的执行是两码事(fn和fn()的区别);函数的执行与函数的定义地方无关;这个一定要理解的;【图】

      闭包

      作用域就是闭包;我理解的是相同的意思;只是不同人对这个机制的叫法不同;闭包是一种机制;并不是某种形式或者概念;最大的闭包就是window,我们可以把window当做一个闭包;

      高程三第192小结中的解释;

      权威指南182页中对闭包的解释;

      权威指南解释:函数的执行依赖于变量作用域,这个作用域是函数定义时决定的,而不是函数调用时决定的

      批注;函数对象可以通过作用域链相互关联起来;函数体内部的变量都可以保存在函数作用域内,这种特性叫闭包; 批注:这里和作用域链有关系的,和闭包没有关系的;函数的作用域是谁,和在哪运行没有关系;只和在哪儿定义有关系;

      如下代码;

      var a=0;
      function fn(){
          var a=1;
          function fm(){
              console.log(a);
          }
          return fm;
      }
      var testFn1=fn();//hanshu这个变量就相当于fm函数;
      testFn1();//相当于fm函数运行;此时输出的是1;而不是0;虽然是在window中运行的;但是在fn中定义的;所以a找到的是fm上一级作用域fm的a;而不是window中的a;
      

      高程三解释:当在函数内部定义了其它函数时,就创建了闭包;闭包有权访问包含函数内部的所有变量;(其实只要函数运行了,就形成了封闭的作用域,就是闭包了)

      作用域不销毁的情况|内存释放

      作用域不销毁的总结:当函数内return一个引用数据类型;并且函数外面有一个变量接收这个引用数据类型;此时的作用域是不销毁的;

      堆内存

      对象数据类型或者函数数据类型在定义的时候首先都会开辟一个堆内存,堆内存有一个引用的地址,如果外面有变量等指到了这个地址,我们就说这个内存被占用了,就不能销毁了;

      堆内存释放的问题->堆内存用来存储引用数据类型值的

      [谷歌]:浏览器会每隔一段时间,看我们的堆内存是否还有其他的东西引用着,如果还在被占用着,浏览器不会进行处理;但是如果我们的堆内存已经没有任何的东西占用了,那么浏览器会把这个堆内存进行回收释放 [IE和火狐]:开辟了一个堆内存,我们有一个占用的时候浏览器记一个数(记录有多少个占用这个内存),当我们减少引用的时候,浏览器会把记数减1,当记的数字减为0的时候,浏览器会把我们的堆内存回收释放

      var obj1 = {name: "张三"};
      var obj2 = obj1;
      //我们想要让堆内存释放/销毁,只需要把所有引用它的变量值赋值为null即可,如果当前的堆内存没有任何东西被占用了,那么浏览器会在空闲的时候把它销毁...
      obj1 = null;
      obj2 = null;
      

      [优化]

      对于堆内存的释放,我们在JS代码编写的时候,对于没有用的堆内存,我们手动给变量赋值为null,来取消所有对这个堆内存的占用,这样浏览器就可以把没有被占用的堆内存进行回收释放…

      栈内存释放的问题->是用来提供JS代码执行环境的

      • 1)[window全局作用域]

      全局作用域在打开当前页面的时候就会生成,只有在关闭当前页面后全局作用域才会销毁 ->全局作用域属于不销毁的作用域

      • 2)私有的作用域(只有函数执行会产生私有的作用域)

      一般情况下,函数执行会形成一个新的私有的作用域,接下来在私有的作用域中完成:形参赋值->预解释->代码从上到下执行,当代码执行完成后我们当前形成这个私有的作用域会"立即销毁";

      但是还是存在特殊的情况的,作用域不销毁:

      当前私有作用域中的部分内存被作用域以外的东西占用了,那么当前的这个作用域就不能销毁了

      - a、函数执行时返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下这个私有作用域就"不能销毁了"; ->如果A这个作用域不销毁,那么它里面的私有变量等都不会销毁;
      
          function fn() {
            var num = 100;
            return function () {
             }
          }
          var f = fn();//fn执行形成的这个私有的作用域就不能再销毁了
      
      - b、在一个私有的作用域中给DOM元素的事件绑定方法,一般情况下我们的私有作用域都不销毁
      
          var oDiv = document.getElementById("div1");
          ~function () {
              oDiv.onclick = function () {}
          }();//当前自执行函数形成的这个私有的作用域也不销毁
      
      - c、下述情况属于不立即销毁->fn返回的函数没有被其他的东西占用,但是还需要执行一次呢,所以暂时不销毁,当返回的值执行完成后,浏览器会在空闲的时候把它销毁了 ->"不立即销毁"
      
          function fn() {
              var num = 100;
              return function () {}
          }
          fn()();//首先执行fn,返回一个小函数对应的内存地址,然后紧接着让返回的小函数再执行;
      

      闭包选项卡的写法,就是通过这个原理来写的;但是闭包没有自定义属性来的性能好;

      例一:

      function fn() {
        var i = 10;
        return function (n) {
          console.log(n + (++i));
        }
      }
      var f = fn();
      f(10);//->21
      f(20);//->32
      fn()(10);//->21
      fn()(20);//->31
      

      例二;

      function fn(i) {
          return function (n) {
            console.log(n + i++);
          }
        }
        var f = fn(13);
        f(12);//->25
        f(14);//->28
        fn(15)(12);//->27
        fn(16)(13);//->29
      

      ++i和i++

      都是自身累加1,在和其他的值进行运算的时候是有区别的

      • i++:先拿i的值进行运算,运算完成本身在+1

      • ++i:先本身累加1,然后拿累加完成的结果去运算

        var i = 5; console.log(1 + i++);//6 console.log(1 + (++i));//8 console.log(2 + (i++) + (++i) + (++i) + (i++));//38 console.log(i);//11

      思考题:用闭包作用域的方式实现选项卡循环绑定事件的处理

      目录
      目录