你真的弄懂闭包了么?

919 阅读5分钟

这是我参与 8 月更文挑战的第 4 天,活动详情查看: 8月更文挑战

MDN中的解释为:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念

闭包有三个特性:

  1. 函数嵌套函数
  2. 函数内部可以引用外部的参数和变量
  3. 参数和变量不会被F垃圾回收机制回收

闭包有两个常用的用途。

  1. 使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  2. 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。

代码示例

创建闭包

在 inner 函数中,我们可以通过作用域链访问到 a 变量,因此这就可以算是构成了一个闭包,因为 a 变量是其他函数作用域中的变量。

function outer(){
    var a = 1;
    function inner(){
        console.log(a);
    }
    inner(); // 1
}
outer();

使用闭包实现每隔一秒打印 1,2,3,4

// 打印出5个5
for( var i = 0; i < 5; i++ ) {
    setTimeout(() => {
        console.log( i );
    }, 1000)
}

// 使用闭包实现
for (var i = 0; i < 5; i++) {
    (function (i) {
        setTimeout(function() {
            console.log(i);
        }, i * 1000);
    })(i)
}

// 使用 let 块级作用域
for(let i = 0; i < 5; i++){
    setTimeout(function() {
        console.log(i);
    }, i * 1000);
}

弄懂以下代码,你就弄懂了闭包的原理

代码1


function makeAdder(x) {
    return function(y) {
        return x + y;
    };
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

代码2


function f1(){
    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }
    return f2;
}

var result=f1();
result(); // 999
nAdd();
result(); // 1000

result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是nAdd=function(){n+=1}这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

代码3


 var name = "The Window";

  var object = {

    name : "My Object",

    getNameFunc : function(){

      return function(){

        return this.name;

     };

    }

};

alert(object.getNameFunc()()); //"The Window"

this对象是在运行时基于函数的执行环境绑定的,在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性。每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments.内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。


var name = "The Window";  

var object = { 
    name: "My Object",
    getNameFunc: function() {
        var that = this; 
        return function() {
            return that.name; 
        }; 
    }
};

alert(object.getNameFunc()()); //"My Object"

在定义匿名函数之前,我们把this对象赋值给了一个名叫that的变量,而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getNameFunc()()就返回了"My Object"。(this和arguments也存在同样的问题,如果想访问作用域中的arguments对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。)

代码3

function outerFun(){
    var a=0;
    function innerFun(){
        a++;
        alert(a);
    }
    return innerFun; //注意这里
}

var obj=outerFun();

obj(); //结果为1

obj(); //结果为2

var obj2=outerFun();

obj2(); //结果为1

obj2(); //结果为2

代码5


function outerFun(){
    //没有var
    a =0;

    alert(a);
}

var a=4;

outerFun(); //0

alert(a); //0

作用域链是描述一种路径的术语,沿着该路径可以确定变量的值 .当执行a=0时,因为没有使用var关键字,因此赋值操作会沿着作用域链到var a=4; 并改变其值.

代码6


function a() {
    var i = 0;
    function b() { alert(++i); }
    return b;
}

var c = a();
c(); //1
c() //2