深入理解闭包

古老定义

  闭包(closure),是指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来

  那这样说来,包含变量的函数就是闭包

//按照古老定义,包含变量n的函数foo就是闭包
function foo() {
    var n = 0;
}
console.log(n)//Uncaught ReferenceError: n is not defined

定义一

  闭包是指可以访问其所在作用域的函数

  那这样说来,需要通过作用域链查找变量的函数就是闭包

//按照定义一的说法,需要通过作用域链在全局环境中查找变量n的函数foo()就是闭包
var n = 0;
function foo() {
    console.log(n)//0
}
foo();

 定义二

  闭包是指有权访问另一个函数作用域中的变量的函数

  那这样说来,访问上层函数的作用域的内层函数就是闭包

//按照定义二的说法,嵌套在foo函数里的bar函数就是闭包
function foo(){
    var a = 2;
    function bar(){
        console.log(a); // 2
    }
    bar();
}
foo();

定义三

  闭包是指在函数声明时的作用域以外的地方被调用的函数

  在函数声明时的作用域以外的地方调用函数,需要通过将该函数作为返回值或者作为参数被传递

【1】返回值

//按照定义三的说法,在foo()函数的作用域中声明,在全局环境的作用域中被调用的bar()函数是闭包
function foo(){
    var a = 2;
    function bar(){
        console.log(a); //2
    }
    return bar;
}
foo()();

  可以简写为如下表示:

function foo(){
    var a = 2;
    return function(){
        console.log(a);//2
    }
}
foo()();

【2】参数

//按照定义三的说法,在foo()函数的作用域中声明,在bar()函数的作用域中被调用的baz()函数是闭包
function foo(){
    var a = 2;
    function baz(){
        console.log(a); //2
    }
    bar(baz);
}
function bar(fn){
    fn();
}

  因此,无论通过何种手段,只要将内部函数传递到所在的词法作用域以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包

IIFE(Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数

 IIFE是不是闭包呢?

  foo()函数在全局作用域定义,也在全局作用域被立即调用,如果按照定义一的说法来说,它是闭包。如果按照定义二和定义三的说法,它又不是闭包

var a = 2;
(function foo(){
    console.log(a);//2
})();

  还有一个更重要的原因是,在requireJS出现之前,实现模块化编程主要通过IIFE,而在IIFE中常见的操作就是通过window.fn = fn来暴露接口,而这个fn就是闭包,而IIFE只是一个包含闭包的函数调用

(function(){
    var a = 0;
    function fn(){
        console.log(a); 
    }
    window.fn = fn;
})()
fn();
  严格来说,闭包需要满足三个条件:【1】访问所在作用域;【2】函数嵌套;【3】在所在作用域外被调用

下面按照代码执行流的顺序对该图示进行详细说明

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz();

  【1】代码执行流进入全局执行环境,并对全局执行环境中的代码进行声明提升(hoisting)

closure2

  【2】执行流执行第9行代码var baz = foo();,调用foo()函数,此时执行流进入foo()函数执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在两个执行环境,foo()函数为当前执行流所在执行环境

closure3

  【3】执行流执行第2行代码var a = 2;,对a进行LHS查询,给a赋值2

closure4

  【4】执行流执行第7行代码return bar;,将bar()函数作为返回值返回。按理说,这时foo()函数已经执行完毕,应该销毁其执行环境,等待垃圾回收。但因为其返回值是bar函数。bar函数中存在自由变量a,需要通过作用域链到foo()函数的执行环境中找到变量a的值,所以虽然foo函数的执行环境被销毁了,但其变量对象不能被销毁,只是从活动状态变成非活动状态;而全局执行环境的变量对象则变成活动状态;执行流继续执行第9行代码var baz = foo();,把foo()函数的返回值bar函数赋值给baz

closure5

  【5】执行流执行第10行代码baz();,通过在全局执行环境中查找baz的值,baz保存着foo()函数的返回值bar。所以这时执行baz(),会调用bar()函数,此时执行流进入bar()函数执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在三个执行环境,bar()函数为当前执行流所在执行环境

  在声明提升的过程中,由于a是个自由变量,需要通过bar()函数的作用域链bar() -> foo() -> 全局作用域进行查找,最终在foo()函数中也就是代码第2行找到var a = 2;,然后在foo()函数的执行环境中找到a的值是2,所以给a赋值2

closure6

  【6】执行流执行第5行代码console.log(a);,调用内部对象console,并从console对象中log方法,将a作为参数传递进入。从bar()函数的执行环境中找到a的值是2,所以,最终在控制台显示2

closure7

  【7】执行流执行第6行代码},bar()的执行环境被弹出执行环境栈,并被销毁,等待垃圾回收,控制权交还给全局执行环境

closure8

  【8】当页面关闭时,所有的执行环境都被销毁

 

总结

  从上述说明的第5步可以看出,由于闭包bar()函数的原因,虽然foo()函数的执行环境销毁了,但其变量对象一直存在于内存中,就是为了能够使得调用bar()函数时,可以通过作用域链访问到父函数foo(),并得到其变量对象中储存的变量值。直到页面关闭,foo()函数的变量对象才会和全局的变量对象一起被销毁,从而释放内存空间

  由于闭包占用内存空间,所以要谨慎使用闭包。尽量在使用完闭包后,及时解除引用,以便更早释放内存

//通过将baz置为null,解除引用
function foo(){
    var a = 2;
    function bar(){
        console.log(a);//2
    }
    return bar;
}
var baz = foo();
baz();        
baz = null;
/*后续代码*/

常见的一个循环和闭包的错误详解

关于常见的一个循环闭包的错误,很多资料对此都有文字解释,但还是难以理解。本文将以执行环境图示的方式来对此进行更直观的解释,以及对此类需求进行推衍,得到更合适的解决办法

 犯错

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//2    

  以上代码的运行结果是2,而不是预想的0。接下来用执行环境图示的方法,详解到底是哪里出了问题

commonError1

  执行流首先创建并进入全局执行环境,进行声明提升过程。执行流执行到第10行,创建并进入foo()函数执行环境,并进行声明提升。然后执行第2行,将arr赋值为[]。然后执行第3行,给arr[0]和arr[1]都赋值为一个匿名函数。然后执行第8行,以arr的值为返回值退出函数。由于此时有闭包的存在,所以foo()执行环境并不会被销毁

commonError2

  执行流进入全局执行环境,继续执行第10行,将函数的返回值arr赋值给bar

commonError3

  执行流执行第11行,访问bar的第0个元素并执行。此时,执行流创建并进入匿名函数执行环境,匿名函数中存在自由变量i,需要使用其作用域链匿名函数 -> foo()函数 -> 全局作用域进行查找,最终在foo()函数的作用域找到了i,然后在foo()函数的执行环境中找到了i的值2,于是给i赋值2

commonError4

  执行流接着执行第5行,以i的值2作为返回值返回。同时销毁匿名函数的执行环境。执行流进入全局执行环境,接着执行第11行,调用内部对象console,并找到其方法log,将bar0的值2作为参数放入该方法中,最终在控制台显示2

   由此我们看出,犯错原因是在循环的过程中,并没有把函数的返回值赋值给数组元素,而仅仅是把函数赋值给了数组元素。这就使得在调用匿名函数时,通过作用域找到的执行环境中储存的变量的值已经不是循环时的瞬时索引值,而是循环执行完毕之后的索引值

IIFE

  由此,可以利用IIFE传参和闭包来创建多个执行环境来保存循环时各个状态的索引值。因为函数传参是按值传递的,不同参数的函数被调用时,会创建不同的执行环境

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = (function fn(j){
            return function test(){
                return j;
            }
        })(i);
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0    

commonError5

 块作用域   ------- es6的let来声明变量

  使用IIFE还是较为复杂,使用块作用域则更为方便

  由于块作用域可以将索引值i重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值,相当于为每一次索引值都创建一个执行环境

function foo(){
    var arr = [];
    for(let i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0  

闭包的8种形式

 根据闭包的定义,我们知道,无论通过何种手段,只要将内部函数传递到所在的词法作用域以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。接下来,本文将详细介绍闭包的8种形式

返回值

  最常用的一种形式是函数作为返回值被返回

var F = function(){
    var b = 'local';
    var N = function(){
        return b;
    }
    return N;
}
console.log(F()());

函数赋值

  一种变形的形式是将内部函数赋值给一个外部变量

var inner;  //内部函数赋值给定义在外部的inner变量
var F = function(){
    var b = 'local';
    var N = function(){
        return b;
    };
    inner = N;
};
F();
console.log(inner());

函数参数

  闭包可以通过函数参数传递函数的形式来实现

var Inner = function(fn){
    console.log(fn());
}
var F = function(){
    var b = 'local';
    var N = function(){
        return b;
    }
    Inner(N);
}
F();

IIFE

  由前面的示例代码可知,函数F()都是在声明后立即被调用,因此可以使用IIFE来替代。但是,要注意的是,这里的Inner()只能使用函数声明语句的形式,而不能使用函数表达式。详细原因移步至此

function Inner(fn){
    console.log(fn());
}
(function(){
    var b = 'local';
    var N = function(){
        return b;
    }
    Inner(N);
})();

 

循环赋值

  在闭包问题上,最常见的一个错误就是循环赋值的错误。关于其错误原因的详细解释移步至此

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//2    

  正确的写法如下

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = (function fn(j){
            return function test(){
                return j;
            }
        })(i);
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0    

 

g(s)etter

  我们通过提供getter()和setter()函数来将要操作的变量保存在函数内部,防止其暴露在外部

var getValue,setValue;
(function(){
    var secret = 0;
    getValue = function(){
        return secret;
    }
    setValue = function(v){
        if(typeof v === 'number'){
            secret = v;
        }
    }
})();
console.log(getValue());//0
setValue(1);
console.log(getValue());//1

 

迭代器

  我们经常使用闭包来实现一个累加器

var add = (function(){
    var counter = 0;
    return function(){
        return ++counter; 
    }
})();
console.log(add())//1
console.log(add())//2  

  类似地,使用闭包可以很方便的实现一个迭代器

function setup(x){
    var i = 0;
    return function(){
        return x[i++];
    }
}
var next = setup(['a','b','c']);
console.log(next());//'a'
console.log(next());//'b'
console.log(next());//'c'

 

区分首次

var firstLoad = (function(){
  var _list = [];
  return function(id){
    if(_list.indexOf(id) >= 0){
      return false;
    }else{
      _list.push(id);
      return true;
    }
  }
})();

firstLoad(10);//true
firstLoad(10);//false
firstLoad(20);//true
firstLoad(20);//false

 

缓存机制

  通过闭包加入缓存机制,使得相同的参数不用重复计算,来提高函数的性能

  未加入缓存机制前的代码如下

var mult = function(){
  var a = 1;
  for(var i = 0,len = arguments.length; i<len; i++){
    a = a * arguments[i];
  }
  return a;
}

  加入缓存机制后,代码如下

var mult = function(){
  var cache = {};
  var calculate = function(){
    var a = 1;
    for(var i = 0,len = arguments.length; i<len; i++){
      a = a * arguments[i];
    }
    return a;
  };
  return function(){
    var args = Array.prototype.join.call(arguments,',');
    if(args in cache){
      return cache[args];
    }

    return cache[args] = calculate.apply(null,arguments);
  }
}()

 

img对象

  img对象经常用于数据上报

var report = function(src){
  var img = new Image();
  img.src = src;
}
report('http://xx.com/getUserInfo');

  但是,在一些低版本浏览器中,使用report函数进行数据上报会丢失30%左右的数据,也就是说,report函数并不是每一次都成功地发起了HTTP请求

  原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随即被销毁,而此时或许还没来得及发出HTTP请求,所以此次请求就会丢失掉

  现在把img变量用闭包封闭起来,就能解决请求丢失的问题

var report = (function(){
  var imgs = [];
  return function(src){
    var img = new Image();
    imgs.push(img);
    img.src = src;
  }
})()
report('http://xx.com/getUserInfo');

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页