JavaScript闭包

闭包:指的是一种函数,有权访问另一个函数(外部函数)作用域中的变量的函数。创建闭包的方式,就是在一个函数内部创建另一个函数。

1、执行环境

每个函数都有自己的执行环境,环境中定义了所有的变量和函数。某个执行环境中的所有的代码执行完毕后,该执行环境被销毁,保存在其中的所有变量和函数定义随之被销毁。当执行流进入一个函数后,函数的环境会被堆入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

2、变量对象

每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

3、作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链。用途是保证对执行环境有权访问的所有变量和函数进行有序的访问。当某个函数被调用时,会创建一个执行环境(execution context)和相应的作用域链,然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,函数自身的活动对象位于第一位,外部函数的变量对象始终位于第二位,外部的外部函数的变量对象处于第三位...,直到作为作用域链终点的全局执行环境对象,全局环境的变量对象始终是作用域链的最后一个变量对象。

 function add(val1, val2) {
     return val1 + val2;
 }
 var res = add(10, 20);

       以上代码先定义的add函数,然后又在全局中调用。当调用add()函数时,会创建一个包含arguments、val1、val2的活动对象,全局执行环境的变量对象(包含res和add)在add函数作用域链中位于第二位。

       一般情况下,每个函数的执行环境都以一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像add()函数的执行环境中的变量对象只在函数执行的过程中有效,函数执行结束后变量对象被销毁。对这个例子而言,add()函数的作用域中包含两个变量对象:自身活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,只引用但不实际包含变量对象。

4、闭包

       但是闭包的情况特殊,在一个函数(外部函数)中嵌套另一个函数(内部函数),内部函数会把外部函数的活动对象添加到自己的作用域链中,在内部函数被作为外部函数的返回值返回时,它的作用域链被初始化为包含外部函数的活动对象和全局对象,这样,内部函数就可以访问外部函数定义的所有变量。更重要的是,外部函数执行完后,其作用域链会被销毁,但是活动对象仍然存在,因为内部函数的作用域链中还在引用这个活动对象,直到内部函数执行完后,和内部函数的活动对象一起销毁。

function A(param){
    var vara = 1;   
    function B(){
        var varb = 2;
        console.log("----Function B----------")
        console.log(vara); //函数B中访问A函数中定义的变量
        console.log(param); //A函数中传进来的变量
        console.log(varb); //访问自身函数内定义的变量
    }
    B();
    console.log("----Function A----------")
    console.log(vara); //访问自身函数内定义的变量
    console.log(param); //A函数中传进来的变量
    console.log(varb); //访问B函数中定义的变量--异常
}
A("hello");

       由此可见嵌套函数(B)可以继承容器函数(A)的参数和变量,但是嵌套函数(B)中的变量对于他的容器函数来说却是B私有的,也就是说 A 无法访问 B 中定义的变量。换句话说,B 函数形成了一个相对独立的环境(空间)使得它自身的变量只能由它自己来访问,但是 A 函数里的变量 B 也可以访问,这里嵌套函数 B 就形成了一个闭包。有一句话很适合 B 来说 “你的就是我的,我的还是我的”。

       从语法上看是函数 A 包含着函数 B,但是从作用域上来看是函数 B 的作用域包含着函数 A 的作用域,关系如下图所示:

闭包的作用(优点)

  • 内部函数可以访问外部函数的作用域中所有变量和函数;但是外部函数不能访问内部函数的变量;
  • 始终在内存中保存外部变量,当外部函数执行完后,外部函数的执行环境就会立即销毁,但是其活动对象可能任然存在,因为此时内部函数还在引用外部函数的活动对象,只有内部函数也执行完,才会随着内部函数一起销毁。
  • 全局变量可能会造成命名冲突,使用闭包不用担心这个问题,因为变量私有化,加强了封装性,这样保护变量的安全。
function A(a){
    function B(b){
        return a + b;
    }
    return B;
}
var C = A(1);
var result = C(2);
console.log(result);//输出结果 3

函数 B 形成了一个闭包,A 函数调用之后返回函数 B 的引用。执行 C 之后发现结果等于3,这也就说明了我们调用 A 的时候 传进去的参数 1 没有被销毁,而是被保存起来了,这就是闭包保存变量的特点。当 B 没有再被引用的时候,就会被销毁。

5、闭包的副作用

  • 由于闭包是驻留在内存中,会增大内存使用量,使用不当很容易造成内存泄露,降低程序的性能
  • 闭包只能取得外部函数中任何变量的最后一个值,即如果内部函数的变量值是由外部函数循环传入的,这个值始终都是外部函数最后一次循环所得的值。
      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){//注:i是outer()的局部变量
            result[i] = function(){
               return i;
            }
         }
         return result;//返回一个函数对象数组
         //这个时候会初始化result.length个关于内部函数的作用域链
      }
      var fn = outer();
      console.log(fn[0]());//result:2
      console.log(fn[1]());//result:2

这个函数会返回一个函数数组。表面看,似乎每个函数会返回自己的索引值,即位置0的函数会返回0,位置1的函数返回1,以此类推。但实际上,每个函数都返回10,因为每个函数的作用域链中都保存着f()外部函数的活动对象,所以它们引入的都是同一个变量 i。当f() 外部函数调用后,变量 i 的值是10,此时再调用内部函数f1(),它们引入的都是同一个变量 i 。

原因:可以从上图中看到此时内部函数result[0]的活动对象中更本没有这个i变量,于是沿着作用域链向后查找,在作用域链包含的外部变量中找到了i,此时i=2,而这个变量i是外部函数执行结束后将最终值保存在内存里的结果,由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。

解决方法:可以把i当成参数传进去,这样一调用函数生成的活动对象内的arguments就有当前i的副本。通过创建另一个自调用执行函数(闭包)

function outer() {
    var result = new Array();
    for (var i = 0; i < 2; i++) {//注:i是outer()的局部变量
        result[i] = (
            function (num) {
                return function () {
                    return num;
                }
            }
        )(i)
    }
    return result;//返回一个函数对象数组
    //这个时候会初始化result.length个关于内部函数的作用域链
}

var fn = outer();
console.log(fn[0]());//result:0
console.log(fn[1]());//result:1
在这里我们定义了一个匿名函数,并将立即执行匿名函数的结果赋给数组。这个匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时传入变量 i ,变量i 会复制给num。而在这个匿名函数内部,又创建一个访问num的闭包。
或者使用let i
function outer() {
    var result = new Array();
    for (let i = 0; i < 2; i++) {//注:i是outer()的局部变量
        result[i] = function () {
            return i;
        }
    }
    return result;//返回一个函数对象数组
    //这个时候会初始化result.length个关于内部函数的作用域链
}

var fn = outer();
console.log(fn[0]());//result:2
console.log(fn[1]());//result:2

6、闭包中的this

对于闭包的痛点在于,闭包的this默认绑定到window对象,但又常常需要访问嵌套函数的this,所以常常在嵌套函数中使用var that = this,然后在闭包中使用that替代this,使用作用域查找的方法来找到嵌套函数的this值 

var a = 0;
function foo(){
    function test(){
        console.log(this.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//0

使用var that = this;将外部函数的this对象赋值给外部函数的一个变量,这个变量就会由于闭包的的特点被保存到内部函数的作用域链中,内部函数就可以通过作用域链中的that访问到外部函数的this变量,

var a = 0;
function foo(){
    var that = this;
    function test(){
        console.log(that.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//2

 

 

 

 

 

 

 

 

 

 

 

版权声明:本文为Rnger原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Rnger/article/details/80487672