js小记:作用域链 和 闭包

这篇文章主要重点是 闭包,只需要读懂 闭包 那节的例1、例2闭包你就懂了。

文章讲解思路:首先我们讲解下 不同语言的作用域解析方式,其中js用的是静态作用域(其实就是作用域链),然后我们讲讲作用域链,最后我们再开始正题“闭包及其应用”。


一、两种解析

变量作用域两种解析方式:

  • 静态作用域:js使用
  • 动态作用域:java、c++等使用

1、静态作用域(作用域链):

又称词法作用域。使用词法环境来管理。
一段代码执行前,先初始化词法环境。
下面代码若使用静态作用域:

var x = 10;
function foo(){
    console.log(x);
}
function bar(){
    var x = 20;
    foo();
}
bar();//10

看下面作用域链:执行bar函数时,在bar的作用域(scope)中找不到foo函数,然后再到全局作用域(global scope)中找,正好全局作用域中有,然后执行foo;
执行foo时又找不到x变量,然后到全局作用域中找,发现x=10,然后打印 10
这里写图片描述


2、动态作用域:

程序运行时决定。动态作用域一般是使用动态栈来管理。
下面代码若使用动态作用域:

var x = 10;
function foo(){
    console.log(x);
}
function bar(){
    var x = 20;
    foo();
}
bar();//20

看下图动态栈:在执行过程中,
(1)首先var x = 10,把x:10放入栈内;
(2)之后再依次执行foo,bar函数的声明,我们再把foo,bar函数放入栈内
(3)在执行bar()时,首先执行var x = 20,我们把x:20放入栈内
(4)然后执行bar函数里面的foo函数,由于foo要打印x变量,所以我们在栈里找到最近的x值。为20,所以打印结果为20.
这里写图片描述


二、作用域链

作用域链的实质就是一个指向 变量对象/活动对象 的指针列表,它只是引用 不包含实际对象。

# 一些概念:
  • 执行环境:执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。全局环境是最外围的执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样(Web浏览器中,全局执行环境是window对象)。
    某个执行环境中所有代码执行完后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。

  • 变量对象 / 活动对象:每个执行环境都有一个变量对象,环境中定义的所有变量和函数都保存在这个对象中。如果这个环境是函数,则将其 活动对象 作为变量对象。

# 作用域链:

作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其 活动对象 作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

例1:

var color = "blue";

function changeColor(){
    if (color === "blue"){
        color = "red";
    } else {
        color = "blue";
    }
}

console.log(changeColor());

在这个简单的例子中,函数 changeColor() 的作用域链包含两个对象:它自己的变量对象(其中定义着 arguments 对象)和全局环境的变量对象。可以在函数内部访问变量 color,就是因为可以在这个作用域链中找到它。

例2:

var color = "blue";

function changeColor(){
    var anotherColor = "red";

    function swapColors(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;

        // 这里可以访问color、anotherColor和tempColor
    }

    // 这里可以访问color和anotherColor,但不能访问tempColor
    swapColors();
}

// 这里只能访问color
changeColor();

以上代码共涉及3个执行环境:全局环境、changeColor() 的局部环境和 swapColors() 的局部环境。全局环境中有一个变量 color 和一个函数 changeColor()。changeColor() 的局部环境中有一个名为 anotherColor 的变量和一个名为 swapColors() 的函数,但它也可以访问全局环境中的变量 color。swapColors() 的局部环境中有一个变量 tempColor,该变量只能在这个环境中访问到。无论全局环境还是 changeColor() 的局部环境都无权访问 tempColor。然而,在 swapColors() 内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。下图形象地展示了前面这个例子的作用域链。
这里写图片描述
上图中的矩形表示特定的执行环境。其中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。对于这个例子中的 swapColors() 而言,其作用域链中包含3个对象:swapColors() 的变量对象、changeColor() 的变量对象和全局变量对象。swapColors() 的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜索上一级作用域链。changeColor() 的作用域链中只包含两个对象:它自己的变量对象和全局变量对象。这也就是说,它不能访问 swapColors() 的环境。函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。


三、闭包

读懂例1、例2,闭包你就懂了!

闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量(作用域链),另一个就是让这些变量的值始终保持在内存中
怎么来理解这句话呢?请看下面的代码。

例1:

function add(){            //function add(){  //一般这么写
    var i = 0;             //   var i = 0;
    function add2(){       //   return function(){
        console.log(++i);  //      console.log(++i);
    }                      //   }
    return add2;           //}
}
var f = add();
f();//1
f();//2

在这段代码中,f 实际上就是函数 add2。它一共运行了两次,第一次的值是 1,第二次的值是 2。这证明了,函数 add 中的局部变量 i 一直保存在内存中,并没有在 add 调用后被自动清除。

为什么会这样呢?原因就在于add 是 add2 的父函数,而 add2 被赋给了一个全局变量 f ,这导致 add2 始终在内存中,而 add2 的存在依赖于 add,因此 add 也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
注:并不是add 的所有变量都在内存里,而是 add2 所用到的 add 中的变量在内存中。
看下面例子:

例2:

 function add(){                   //function add(){  //一般这么写
      var i = 0;                   //    var i = 0; 
      function add2(){             //    return function(){
           var a = 0;              //        var a = 0;
           console.log('a:',++a);  //        console.log('a:',++a);
           console.log('i:',++i);  //        console.log('i:',++i);
      }                            //    }
      return add2;                 //}
  }
  var f = add();
  f();//a:1  i:1
  f();//a:1  i:2

我们发现并不是 add 的所有变量都保存在内存中(eg: 变量a),而是 add2 所用到的 add 中的变量 i 保存在内存中。


使用闭包的注意点:

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除或设置为 null,断开变量和内存的联系
  • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。

思考题:为什么这个输出就都是1?

 var f = function(){
     var i = 0;
     return function(){
         console.log(++i);
     }; 
 };
 f()();//1
 f()();//1

解答:
上面很明显没实现闭包。这里的变量 f 保存的并不是return 的函数, 我们需要将 return 的函数 赋给一个全局变量,然后分析同上。
闭包应当是一个封闭function内的数据保留,然后由内向外提供一个外部调用接口。这里的问题是外部接口并不是由封闭区域提供。
可以正确工作的例子

var f = (function(){   //这里将return 的函数赋与了全局变量f
    var i = 0;
    return function(){
        console.log(++i);
    }; 
})();
f();//1
f();//2

或(脑子抽筋了才用下面的写法)

var f = (function(){    
    var i= 0; 
    return function(){      
       return function(){
            console.log(++i);   
        }    
    }
 })() 
f()() ;
f()() ;

四、闭包应用

1、保存变量现场

有个需求,我们想绑定一堆DOM节点,然后点击任意节点都能弹出对于的i的值,告诉我们这是第几个节点。

看下面代码:我们会发现无论点击第几个节点,弹出都是nodes.length的值,这时我们就要用闭包了。

<li>0</li>
<li>1</li>
<li>2</li>
<script type="text/javascript">
    var aLi = document.getElementsByTagName('li');
    var handle = function(nodes){
        for(var i = 0;i < nodes.length;i++){
            nodes[i].onclick = function(){
                alert(i);
            }
        }
    };
    handle(aLi);
</script>

改进:我们写个闭包closer来保存变量现场就ok了
写法一:

var aLi = document.getElementsByTagName('li');
var handle = function(nodes){
    function closer(i){
        return function(){
            alert(i);
        };
    }
    for(var i = 0;i < nodes.length;i++){
        nodes[i].onclick = closer(i);
    }
};
handle(aLi);

写法二:

var aLi = document.getElementsByTagName('li');
var handle = function(nodes){
    for(var i = 0;i < nodes.length; i++){
        nodes[i].onclick = (function(i){
            return function(){
                alert(i);
            };
        })(i);
    }
};
handle(aLi);


2、封装

封装也就是将可见的 和 不可见的分开,就像面向对象编程,有些变量方法是可以暴露给外部的(public),而有些是不能暴露给外部的。
封装还有一个好处就是能避免内部变量污染外部环境

var observer = (function(){
  var observerList = [];
  return {
      add:function(obj){
          observerList.push(obj);
      },
      empty:function(){
          observerList:[];
      },
      getCount:function(){
          return observerList.length;
      }
  };
})();

上面代码:
(function(){})就是一个闭包,而(function(){})()就是闭包return的对象,我们用observer来保存这个对象。

在外部:

  • 我们不能单独访问数组observerList,这就是不能暴露在外面的。
  • 我们必须通过observer对象的方法,才能访问或操作数组observerList,这些就是能暴露在外面的。

最后我们再来两个检测的例子,看看你是否真的懂了~
练习1:

scope = "stone";

function Func() {
    var scope = "sophie";

    function inner() {
        console.log(scope);
    }
    scope = "tommy";
    return inner;
}

var ret = Func();
ret();    // ?

练习2:

scope = "stone";

function Bar() {
    console.log(scope);
}

function Func() {
    var scope = "sophie";
    return Bar;
}

var ret = Func();
ret();    // ?

答案输出是·····························
············
····························
············
··········
···············
·········
·········
············
···········
·········
········

练习1:tommy
练习2:见下图,在Func中调用Bar,因为Func的作用域没找到Bar,所以到全局作用域中寻找Bar;找到后函数Bar又调用了变量scope,因为Bar作用域中没有变量scope,所以又回到全局作用域中寻找,发现scope=”stone”.
所以答案为 : stone
这里写图片描述

猜你喜欢

转载自blog.csdn.net/b954960630/article/details/80143676
今日推荐