• Javascript 闭包(Closures)


    本文内容

    • 闭包
    • 闭包和引用
    • 参考资料

    闭包是 JavaScript 的重要特性,非常强大,可用于执行复杂的计算,可并不容易理解,尤其是对之前从事面向对象编程的人来说,对 JavaScript 认识和编程显得更难。特别是在看一些开源的 JavaScript 代码时感觉尤其如此,跟看天书没什么区别。

    一般情况下,人们认识一个事物,会根据之前的经验,进行对比和总结,在脑中建立一个模型,从而理解掌握它,但是 JavaScript 与面向对象编程实在“没有可比性”,最明显的是某过于写法,总觉得“怪怪的”,更不用说,其一些高级特性。如果说“对象”在面向对象编程时的出现相当有规律,但是在 JavaScript 中则毫无规律,无处不在,甚至在你意想不到的地方。

    首先看两段代码。

    示例 1:

    var sMessage = "hello world";
     
    function sayHelloWorld() {
        alert(sMessage);
    }
     
    sayHelloWorld();

    示例 2:

    var iBaseNum = 10;
     
    function addNum(iNum1, iNum2) {
        function doAdd() {
            return iNum1 + iNum2 + iBaseNum;
        }
        return doAdd();
    }

    示例 1 和示例 2 都是闭包,只是 2 比 1 复杂,甚至还有更复杂的写法,比如返回多个闭包。

    示例 1,脚本被载入内存后,并没有为函数 sayHelloWorld() 计算变量 sMessage 的值。该函数捕获 sMessage 的值只是为了以后的使用,也就是说,解释程序知道在调用该函数时要检查 sMessage 的值。sMessage 将在函数调用 sayHelloWorld() 时(最后一行)被赋值,显示消息 "hello world"。

    示例 2,函数 addNum() 包括函数 doAdd() (闭包)。内部函数是一个闭包,因为它将获取外部函数的参数 iNum1 和 iNum2 以及全局变量 iBaseNum 的值。 addNum() 的最后一步调用了 doAdd(),把两个参数和全局变量相加,并返回它们的和。

    这里要掌握的重要概念是,doAdd() 函数根本不接受参数,它使用的值是从执行环境中获取的。

    闭包


    闭包,根据 ECMAScript 描述,词法(lexically)表示包括不被计算的变量的函数,函数可以使用函数之外定义的变量,它意味着当前作用域总能够访问外部作用域中的变量。函数是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。函数内部的函数访问其所在函数的变量(局部变量、形参),这些变量会受到内部函数的影响,当其外部函数外被调用时,就会形成闭包。内部的函数会在其外部函数返回后,被执行。

    示例 3:

    function foo() {
        var a = 10;
     
        function bar() {
            a *= 2;
            return a;
        }
        return bar; // 返回内部函数 bar
    }
     
    var baz = foo();
    baz(); // 20

    说明:

    • foo 是 bar 的外部函数,bar 是 foo 的内部函数;a 是 foo 的局部变量;
    • bar 访问 foo 的局部变量 a;
    • foo 返回 bar。
    • bar 在 foo 的外部被调用。

    当执行 baz() 后,闭包使 Javascript 垃圾回收机制不会回收 foo 所占的资源。因为,baz 实际指向 foo 的内部函数 bar,bar 依赖 foo 的局部变量 a。这样,在执行 var baz=foo() 后,baz 实际指向了 bar,而不是 foo。bar 访问了 foo 的局部变量 a,当执行 baz() 后,a 为 20。这就形成了一个闭包。如下图所示:

    201111271751015074

    图 1

    如果把 foo 看作是一个包,根据剪头指示,形成了一个闭包。结果是局部变量 a 的持久性(如示例 4 所示)。下面代码就不是闭包。无论执行多少次,都是显示 20。

    示例 4:

    function foo() {
        var a = 10;
        function bar() {
            alert(a *= 2);
        }
        bar();
    }
    foo(); // 20
    foo(); // 20
    foo(); // 20

    从以上两个示例看,闭包有点类似于面向对象的接口和委托,——只是调用方法而无需知道具体细节。

    示例 5:

    function foo() {
        var a = 10;
        function bar() {
            a *= 2;
            return a;
        }
        return bar;
    }
     
    var baz = foo();
    baz(); // 20
    baz(); // 40
    baz(); // 80
     
    var blat = foo();
    blat(); // 20

     

    闭包和引用


    模拟私有变量

    代码 6:

    function Counter(start) {
        var count = start;
        return {
            increment: function () {
                count++;
            },
     
            get: function () {
                return count;
            }
        }
    }
     
    var foo = Counter(4);
    foo.increment();
    foo.get(); // 5

    这里,Counter 函数返回两个闭包,函数 increment 和函数 get。 这两个函数都维持着 对外部作用域 Counter 的引用,因此总可以访问此作用域内定义的变量 count.

    为什么不能在外部访问私有变量

    因为 JavaScript 中不可以对作用域进行引用或赋值,因此没有办法在外部访问 count 变量,唯一的途径就是通过上面那两个闭包。

    var foo = new Counter(4);
    foo.hack = function() {
        count = 1337;
    };
    上面的代码不会改变定义在 Counter 作用域中的 count 变量的值,因为 foo.hack 没有 定义在那个作用域内。它将会创建或者覆盖全局变量 count。

    循环中的闭包

    一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号,

    for(var i = 0; i < 10; i++) {
        setTimeout(function() {
            console.log(i);  
        }, 1000);
    }

    上面的代码不会输出数字 0 到 9,而是会输出数字 10 十次。

    当 console.log 被调用的时候,匿名函数保持对外部变量 i 的引用,此时for循环已经结束, i 的值被修改成了 10.

    为了得到想要的结果,需要在每次循环中创建变量 i 的拷贝。

    避免引用错误

    为了正确的获得循环序号,最好使用 匿名包裹器(自执行匿名函数)。

    for(var i = 0; i < 10; i++) {
        (function(e) {
            setTimeout(function() {
                console.log(e);  
            }, 1000);
        })(i);
    }

    外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。

    当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

    有另一个方法完成同样的工作;那就是从匿名包装器中返回一个函数。这和上面的代码效果一样。

    for(var i = 0; i < 10; i++) {
        setTimeout((function(e) {
            return function() {
                console.log(e);
            }
        })(i), 1000)
    }

     

    参考资料

     

    下载 Demo

  • 相关阅读:
    使用纯资源DLL文件实现多语言菜单、界面文字、Tooltips等[转]
    用VC++打造有多语言菜单的应用程序[转]
    VC2008以资源形式实现多语言版本[转]
    GetWindowRect与GetClientRect 的区别[转]
    MFC拆分窗口及它们之间的数据交换[转]
    【排序算法】(7)快速排序
    【排序算法】(4)归并排序
    【排序算法】(3)插入排序
    【排序算法】(8)希尔排序
    【排序算法】(2)冒泡排序
  • 原文地址:https://www.cnblogs.com/liuning8023/p/3348823.html
Copyright © 2020-2023  润新知