资讯专栏INFORMATION COLUMN

JS 闭包(closure)

nihao / 3023人阅读

摘要:对数组函数而言,相当于产生了个闭包。关于对象在闭包中使用对象也会导致一些问题。不过,匿名函数的执行环境具有全局性,因此其对象通常指向。由于声明函数时与声明函数时的值是不同的,因此闭包与闭包貌似将会表示各自不同的值。

这几天看到闭包一章,从工具书到各路大神博客,都各自有着不同的理解,以下我将选择性的抄(咳咳,当然还是会附上自己理解的)一些大神们对闭包的原理及其使用文章,当作是自己初步理解这一功能函数的过程吧。

首先先上链接:

简书作者波同学的JS进阶文章系列: 前端基础进阶系列 其他: JS秘密花园 javascript深入理解js闭包 阮一峰《JavaScript标准参考教程》 一不小心就做错的JS闭包面试题

还有一些也很不错,但主要是以应用为主,原理解释没有上面几篇深入,不过作为闭包的拓展应用其实也可以看一看;

JavaScript中的匿名函数及函数的闭包

红皮书《JS高程》的闭包:
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

从这句话我们知道:闭包是一个函数

    function createComparisonFunction(propertyName) {

        return function(obj1,obj2) {
            var value1 = obj1[propertyName];
            var value2 = obj2[propertyName];
            
            if (value1 < value2) {
                return -1;
            } else if (value1 > value2) {
                return 1;
            } else {
                return 0;
            }
        };
    }

这段代码,我们能直接看出,共存在三个作用域,Global、createComparisonFunction、匿名函数funciton,因其JS的作用域链特性,后者能访问自身及前者的作用域。而返回的匿名函数即使在其他地方被调用了,但它仍可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含createComparisonFunction的作用域。我们来深入了解一下,函数执行时具体发生了什么?

当第一个函数被调用时,会创建一个执行环境(Execution Context,也叫执行上下文)及相应的作用域链,并把作用域链赋值给一个特殊的内部属性[[Scope]]。然后,使用this、arguments和其他命名参数的值来初始化函数的活动对象(Activation Object)。但在作用域链中,外部函数的活动对象处于第二位,外部函数的外部函数处于第三位,最后是全局执行环境(Global Context)。

换一个栗子:

    function createFunctions() {
        var result = new Array();
        
        for (var i=0;i<10;i++) {
            result[i] = function() {
                return i;
            };
        }
        return result;
    }
    var arr = createFunctions();
    alert(arr[0]());    // 10
    alert(arr[1]());    // 10

/这个函数返回一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值,位置0的函数返回0,位置1的函数返回1,以此类推。但但实际上,每个函数都返回10,为什么?
数组对象内的匿名函数里的i是引用createFunctions作用域内的,当调用数组内函数的时候,createFunctions函数早已执行完毕。

这图不传也罢了,画得忒丑了。
数组内的闭包函数指向的i,存放在createFunctions函数的作用域内,确切的说,是在函数的变量对象里,for循环每次更新的i值,就是从它那儿来的。所以当调用数组函数时,循环已经完成,i也为循环后的值,都为10;

有人会问,那result[i]为什么没有变为10呢?
要知道,作用域的判定是看是否在函数内的,result[i] = function.......是在匿名函数外,那它就还是属于createFunctions的作用域内,那result[i]里的i就依然会更新

那么如何使结果变为我们想要的呢?也是通过闭包。

    function createFunctions() {
        var result = [];
        
        for (var i=0;i<10;i++) {
            !function(i) {
                result[i] = function() {console.log(i)};
            }(i);
        }
        return result;
    }
    var arr = createFunctions();
    arr[0]();
    arr[1]();
    arr[2]();
    function createFunctions() {
        var result = [];
        function fn(i) {
            result[i] = function() {console.log(i)}
        };
        for (var i=0;i<10;i++) {
            fn(i);
        }
        
        return result;
    }
    var arr = createFunctions();
    arr[0]();
    arr[1]();
    arr[2]();
    var arr = [];
    function fn(i) {
        arr[i] = function() {console.log(i)}
    }
    function createFunctions() {
        for (var i=0;i<10;i++) {
            fn(i);
        }
    }
    fn(createFunctions());
    arr[0]();
    arr[1]();
    arr[2]();

以第一种为例,通过一个立即调用函数,将外函数当前循环的i作为实参传入,并存放在立即调用函数的变量对象内,此时,这个函数立即调用函数和数组内的匿名函数就相当于一个闭包,数组的匿名函数引用了立即调用函数变量对象内的i。当createFuncions执行完毕,里面的i值已经是10了。但是由于闭包的特性,每个函数都有各自的i值对应着。对数组函数而言,相当于产生了10个闭包。

所以能看出,闭包也十分的占用内存,只要闭包不执行,那么变量对象就无法被回收,所以不是特别需要,尽量不使用闭包。

关于this对象
在闭包中使用this对象也会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的;在全局对象中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。(当然可以用call和apply)
    var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function () {
            var bibao = function () {
                return this.name;
            };
            return bibao;
        }
    };
    alert(obj.getName()());            // The Window

先创建一个全局变量name,又创建一个包含name属性的对象。这个对象包含一个方法——getName(),它返回一个匿名函数,而匿名函数又返回this.name。由于getName()返回一个函数,因此调用obj.getName()();就会立即调用它返回的函数,结果就是返回一个字符串。然而,这个例子返回的字符串是"The Window",即全局name变量的值。为什么匿名函数没有取得其波包含作用域(或外部作用域)的this对象呢?

每个函数调用时其活动对象都会自动取得两个特殊变量:thisarguments
内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

    var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function () {
            var that = this;
            return function () {
                return that.name;
            };
        }
    };
    alert(obj.getName()());

thisarguments也存在同样的问题,如果想访问作用域中arguments对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。

    var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function (arg1,arg2) {
            var arg = [];
            arg[0] = arg1;
            arg[1] = arg2;
            function bibao() {
                return arg[0]+arg[1];
            }
            return bibao;
        }
    };
    alert(obj.getName(1,2)())

obj.getName方法保存了其接收到的实参在它的变量对象上,并在执行函数结束后没有被回收,因为返回的闭包函数引用着obj.Name方法里的arg数组对象。使得外部变量成功访问到了函数内部作用域及其局部变量。

在几种特殊情况下,this引用的值可能会意外的改变。

    var name = "The Window";        
    var obj = {
        name:"My Object",
        getName:function () {
            return this.name;
        }
    };

这里的getName()只简单的返回this.name的值。

    var name = "The Window";        
    var obj = {
        name:"My Object",
        getName:function () {
            console.log(this.name);
        }
    };
    obj.getName();                        // "My Object"
    (obj.getName)();                      // "My Object"
    (obj.getName = obj.getName)();    // "The Window"

第一个obj.getName函数作为obj对象的方法调用,则自然其this引用指向obj对象。
第二个,加括号将函数定义之后,作为函数表达式执行调用,this引用指向不变。
第三个,括号内先执行了一条赋值语句,然后在调用赋值后的结果。相当于重新定义了函数,this引用的值不能维持,于是返回"The Window"

闭包与setTimeout()

setTimeout结合循环考察闭包是一个很老的面试题了

    // 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
    for (var i=1; i<=5; i++) { 
        setTimeout( function timer() {
            console.log(i);
        }, i*1000 );
    }
setTimeout的执行与我们平常的JS代码执行不一样,这里需要提到一个队列数据结构执行的概念。 关于setTimeout与循环闭包的思考题

个人理解:由于setTimeout函数的特殊性,须等其他非队列结构代码执行完毕后,这个setTimeout函数才会进入队列执行栈。

用chrome开发者工具分析这段代码,可以先自己分析一次,看看依次弹出什么?
    setTimeout(function() {
        console.log(a);
    }, 0);
    
    var a = 10;
    
    console.log(b);
    console.log(fn);
    
    var b = 20;
    
    function fn() {
        setTimeout(function() {
            console.log("setTImeout 10ms.");
        }, 10);
    }
    
    fn.toString = function() {
        return 30;
    }
    
    console.log(fn);
    
    setTimeout(function() {
        console.log("setTimeout 20ms.");
    }, 20);

    fn();    

答案:

设置断点如图所示,今天刚学Chrome的开发者工具,有哪些使用上的错误还请指出。
我分别给变量a、b、fn函数都设置了观察,变量的值变化将会实时地在右上角中显示,可以看到,在JS解释器运行第一行代码前,变量a、b就已经存在了,而fn函数已经完成了声明。接下来我们继续执行。要注意:蓝色部分说明这些代码将在下一次操作中执行,而不是已经执行完毕。

把第一个setTimeout函数执行完毕后也没有反应。我给三个setTimeout内的匿名函数也加上观察选项,却显示不可使用。

所以,下一次执行会发生什么?对console出b的值,但是b没赋值,右上角也看到了,所以显示undefined。
而console.log(fn)就是将fn函数函数体从控制台弹出,要注意,console会隐式调用toString方法,这个会在后面讲到。

现在第26行之前(不包括26行)的代码都已略过,a,b变量也已得到赋值,继续执行。
重写了toString方法前:

重写后:

toString方法是Object所有,所有由它构造的实例都能调用,现在这个方法被改写并作为fn对象的属性(方法)保留下来。
console会隐式调用toString方法,所以30行的console会弹出30;

继续执行,定义setTimeout函数也是什么没有发生,知道调用fn前。

调用fn,是不是就会执行setTimeout函数呢?其实没有,我们可以看到call stack一栏已经是fn的执行栈了,但是依旧没发生什么。
但是:

当call stack里的环境都已退出,执行栈里没有任何上下文时,三个setTimeout函数就执行了,那这三个时间戳函数那个先执行,那个后执行呢?由设定的延迟时间决定,这个延迟时间是相对于其他代码执行完毕的那一刻。
不信我们可以通过改变延迟时间重新试一次就知道了。

我们在看回原来的闭包代码:

    // 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
    for (var i=1; i<=5; i++) { 
        setTimeout( function timer() {
            console.log(i);
        }, i*1000 );
    }

先确认一个问题,setTimeout函数里的匿名函数的i指向哪儿?对,是全局变量里的i。
setTimeout里的匿名函数执行前,外部循环已经结束,i值已经更新为6,这时setTimeout调用匿名函数,里面的i当然都是6了。

我们需要创建一个能够保存当前i值的"盒子"给匿名函数,使得匿名函数能够引用新创建的父函数。

    // 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
    for (var i=1; i<=5; i++) { 
        !function (i) {
            setTimeout( function timer() {
                console.log(i);
            }, i*1000 );
        }(i);
    }

自调用函数就是那个"盒子"

关于《JavaScript编程全解》中的说明

考虑这个函数:

    function f(arg) {
        var n = 123 + Number(arg);
        function g() {console.log("n is "+n);console.log("g is called");}
        n++;
        function gg() {console.log("n is "+n);console.log("g is called");}
        
        return [g,gg];
    }

调用数组内函数的console结果是什么?

    var arr = f(1);
    
    arr[0]();            // 对闭包g的调用
    // "n is 125"    "g is called"
    
    
    arr[1]();            // 对闭包gg的调用
    // "n is 125"    "gg is called"

函数g与函数gg保持了各自含有局部变量n的执行环境。由于声明函数g时与声明函数gg时的n值是不同的,因此闭包g与闭包gg貌似将会表示各自不同的n值。实际上两者都将表示相同的值。因为它们引用了同一个对象。

即都是引用了,f函数执行环境内变量对象内的n值。当执行f(1)的时候,n值就已经更新为最后计算的值。

防范命名空间的污染 模块:

在JavaScript中,最外层代码(函数之外)所写的名称(变量名与函数名)具有全局作用域,即所谓的全局变量与全局函数。JavaScript的程序代码即使在分割为多个源文件后,也能相互访问其全局名称。在JavaScript的规范中不存在所谓的模块的语言功能。

因此,对于客户端JavaScript,如果在一个HTML文件中对多个JavaScript文件进行读取,则他们相互的全局名称会发生冲突。也就是说,在某个文件中使用的名称无法同时在另一个文件中使用。

即使在独立开发中这也很不方便,在使用他们开发的库之类时就更加麻烦了。
此外,全局变量还降低了代码的可维护性。不过也不能就简单下定论说问题只是由全局变量造成的。这就如同在Java这种语言规范并不支持全局变量的语言中,同样可以很容易创建出和全局变量功能类似的变量。

也就是说,不应该只是一昧地减少全局变量的使用,而应该形成一种尽可能避免使用较广的作用域的意识。对于较广的作用域,其问题在于修改了某处代码之后,会难以确定该修改的影响范围,因此代码的可维护性会变差。 避免使用全局变量

从形式上看,在JavaScript中减少全局变量的数量的方法时很简单的。首先我们按照下面的代码这样预设一下全局函数与全局变量。

    // 全局函数
    function sum(a,b) {
        return Number(a)+Number(b);
    }
    // 全局变量
    var position = {x:2,y:3};
    // 借助通过对象字面量生成对象的属性,将名称封入对象的内部。于是从形式上看,全局变量减少了
    var MyModule = {
        sum:function (a,b) {
            return Number(a)+Number(b);
        },
        position:{x:2,y:3}
    };

    alert(MyModule.sum(3,3));        // 6
    alert(MyModule.position.x);        // 2

上面的例子使用对象字面量,不过也可以像下面这样不使用对象字面量。

    var MyModule = {};            // 也可以通过new表达式生成
    MyModule.sum = function (a,b) {return Number(a)+Number(b);};
    MyModule.position = {x:2,y:3};

这个例子中,我们将MyModule称为模块名。如果完全采用这种方式,对于1个文件来说,只需要一个模块名就能消减全局变量的数量。当然,模块名之间仍然可能产生冲突,不过这一问题在其他程序设计语言中也是一个无法被避免的问题。
通过这种将名称封入对象之中的方法,可以避免名称冲突的问题。但是这并没有解决全局名称的另一个问题,也就是作用域过广的问题。通过MyModule.position.x这样一个较长的名称,就可以从代码的任意一处访问该变量。

通过闭包实现信息隐藏
    // 在此调用匿名函数
    // 由于匿名函数的返回值是一个函数,所以变量sum是一个函数
    var sum = (function () {
        // 无法从函数外部访问该名称
        // 实际上,这变成了一个私有变量
        // 一般来说,在函数被调用之后该名称就无法再被访问
        // 不过由于是在被返回的匿名函数中,所以仍可以继续被使用
        var p = {x:2,y:3};
        
        // 同样是一个从函数外无法被访问的私有变量
        // 将其命名为sum也可以。不过为了避免混淆,这里采用其他名称
        function sum_internal(a,b) {
            return Number(a)+Number(b);
        }
        // 只不过是为了使用上面的两个名称而随意设计的返回值
        return function (a,b) {
            alert("x = "+p.x);
            return sum_internal(a,b);
        }
    })();
    console.log(sum(3,4));
    // "x = 2"
    // "y"

上面的代码可以抽象为下面这种形式的代码。在利用函数作用域封装名称,以及闭包可以使名称在函数调用结束后依然存在这两个特性。这样信息隐藏得以实现。

    (function(){函数体})();

像上面这样,当场调用函数的代码看起来或许有些奇怪。一般的做法是先在某处声明函数,之后在需要时调用。不过这种做法是JavaScript的一种习惯用法,加以掌握。
匿名函数的返回值是一个函数,不过即使返回值不是函数,也同样能采用这一方法。比如返回一个对象字面量以实现信息隐藏的功能。

    var obj = (function() {
        // 从函数外部无法访问该名称
        // 实际上,这是一个私有变量
        var p = {x:2,y:3};
        
        // 这同样是一个无法从函数外部访问的私有函数
        function sum_internal(a,b) {
            return Number(a+b);
        }
        
        // 只不过为了使用上面的两个名称而随意设计的返回值
        return {
            sum:function (a,b) {
                return sum_internal(a,b);
            },
            x:p.x
        };
    })();
    
    alert(obj.sum(3,4));     // 7
    alert(obj.x);            // 2
闭包与类

利用函数作用域与闭包,可以实现访问在控制,上一节中,模块的函数在被声明之后立即就对其调用,而是用了闭包的类则能够在生成实例时调用。即便如此,着厚重那个做法在形式上仍然只是单纯的函数生命。下面是一个通过闭包来对类进行定义的例子

    // 用于生成实例的函数
    function myclass(x,y) {
        return {show:function () {alert(x+" | "+y)}};
    }
    var obj = myclass(3,2);
    obj.show();        // 3 | 2

这里再举一个具体的例子,一个实现了计数器功能的类。

这里重申一下:JavaScript的语言特性没有"类"的概念。但这里的类指的是,实际上将会调用构造函数的Function对象。此外在强调对象是通过调用构造函数生成的时候,会将这些被生成的对象称作对象实例以示区别。

表达式闭包

JavaScript有一种自带的增强功能,称为支持函数型程序设计的表达式闭包(Expression closure)。
从语法结构上看,表达式闭包是函数声明表达式的一种省略形式。可以像下面这样省略只有return的函数声明表达式中的return{}

    var sum = function (a,b) {return Number(a+b)};
    // 可以省略为
    var sum = function (a,b) Number(a+b);

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87034.html

相关文章

  • 谈谈我所理解的闭包js、php、golang里的closure

    摘要:当初看这个解释有点懵逼,理解成闭包就是函数中的函数了。里的闭包最近不满足于只干前端的活,开始用起了。里的闭包最近在学习语言,让我们来看一下语言里的闭包。在中,闭包特指将函数作为值返回的情况,被返回的函数引用了生成它的母函数中的变量。 本人开始接触编程是从js开始的,当时网上很多人说闭包是难点,各种地方对闭包的解释也是千奇百怪。如今开始接触js以外的各种编程语言,发现不光是js,php、...

    betacat 评论0 收藏0
  • 谈谈我所理解的闭包js、php、golang里的closure

    摘要:当初看这个解释有点懵逼,理解成闭包就是函数中的函数了。里的闭包最近不满足于只干前端的活,开始用起了。里的闭包最近在学习语言,让我们来看一下语言里的闭包。在中,闭包特指将函数作为值返回的情况,被返回的函数引用了生成它的母函数中的变量。 本人开始接触编程是从js开始的,当时网上很多人说闭包是难点,各种地方对闭包的解释也是千奇百怪。如今开始接触js以外的各种编程语言,发现不光是js,php、...

    zhoutao 评论0 收藏0
  • 《你不知道的JS》读书笔记---作用域及闭包

    摘要:注此读书笔记只记录本人原先不太理解的内容经过阅读你不知道的后的理解。作用域及闭包基础,代码运行的幕后工作者引擎及编译器。 注:此读书笔记只记录本人原先不太理解的内容经过阅读《你不知道的JS》后的理解。 作用域及闭包基础,JS代码运行的幕后工作者:引擎及编译器。引擎负责JS程序的编译及执行,编译器负责词法分析和代码生成。那么作用域就像一个容器,引擎及编译器都从这里提取东西。 ...

    denson 评论0 收藏0
  • 详解js闭包

    摘要:但闭包的情况不同嵌套函数的闭包执行后,,然后还在被回收闭包会使变量始终保存在内存中,如果不当使用会增大内存消耗。每个函数,不论多深,都可以认为是全局的子作用域,可以理解为闭包。 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 闭包的特性 闭包有三个特性: 1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会...

    Chiclaim 评论0 收藏0
  • 老生常谈之闭包(你不可不知的若干知识点)

    摘要:闭包是什么这是一个在面试的过程中出现的概率为以上的问题,也是我们张口就来的问题。文章推荐我们面试中在被问到闭包这个问题是要注意的几点闭包的延伸,让面试变得 闭包是什么?这是一个在面试的过程中出现的概率为60%以上的问题,也是我们张口就来的问题。但是我们往往发现,在面试的过程中我们的回答并不那么让面试官满意,我们虽然能张口说出一些但是却不能系统的对这个问题进行回答。面试官希望加入自己团队...

    daydream 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<