本文内容

  • 函数内部访问全局变量
  • 函数外部不能直接访问局部变量
  • 函数外部访问局部变量
  • 保护私有成员
  • 持久性
  • 模块化
  • 抽象性

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

一般情况下,人们认识一个事物,会根据之前的经验,进行对比和总结,在脑中建立一个模型,从而理解掌握它,但是 JavaScript 与面向对象编程实在“没有可比性”,最明显的是某过于语法,总觉得“怪怪的”,更不用说,一些高级特性。

因此,从闭包可以做什么开始,将有助于进一步理解闭包。

函数内部访问全局变量


函数内部可以访问全局变量,如下所示:

var baz = 10;

function foo() {

    alert(baz);

}

 

foo();

 

函数外部不能直接访问局部变量


函数外部不能访问局部变量,下面代码将报错:

function foo() {

      var bar = 20;

  }

 

alert(bar);

会报错,因为 bar 是局部变量。函数内部声明的变量,若没有使用 var 关键字,则变量是一个全局变量。

function foo() {

    bar = 20;

}

 

alert(bar);

 

函数外部访问局部变量


实际情况是,我们需要从函数外部访问函数内部的局部变量。大多数情况只是读,而不能修改,修改对代码是不安全的,如下所示:

function foo() {

    var a = 10;

    function bar() {

        a *= 2;

    }

    bar();

    return a;

}

 

var baz = foo(); // 20

// baz(); 不用再调用

其中,a 是 foo 的局部变量,bar 当然可以访问。但如何在 foo 外,调用 bar,并访问 a?另外,此时,baz 不是一个对象,只是一个值,因此,不能再像 baz() 这样调用,也因此只能改变一次 a 的值。

现在,改写上面的代码,如下所示:

function foo() {

    var a = 10;

    function bar() {

        a *= 2;

        return a;

    }

 

    return bar;

}

 

var baz = new foo();

baz(); // 20

baz(); // 40

baz(); // 80

 

var blat = new foo();

blat(); // 20

说明:

  • 现在可以从外部访问 a;
  • JavaScript 的作用域是词法性的。a 是运行在定义它的 foo 中,而不是运行在调用 foo 的作用域中。因此,只要 bar 定义在 foo 中,bar 就能访问 foo 的局部变量 a,即使 foo 已经执行完毕。也就是说,var baz = foo() 执行后,foo 已经执行结束了,局部变量 a 应该不存在了,但之后再调用 baz 发现,a 依然存在——这就是 JavaScript 特色,运行在定义中,而不是运行在调用。这跟 C、C#以及其他编程语言明显不同。闭包,就是在 foo 执行完并返回后,使得 Javascript 垃圾回收机制不收回 foo 所占用的资源,因为 foo 的内部函数 bar 的执行需要 foo 中的变量;
  • var baz = foo() 是 bar 一个引用;var blat= foo() 是另一个。

 

持久性


在“函数外部访问函数内部的局部变量”小节,我们看到,函数被调用多次,私有变量能够保持其持久性,如示例 4 所示,因为,JavaScript 是运行在定义中,而不是运行在调用中的。尽管,变量的作用域仅限于其函数内,无法从外部直接访问,但在多次调用期间,值仍然存在。

因此,闭包可以用来信息隐藏,并应用于需要状态表达的某些编程范型中。

 

保护私有成员


在以上描述,通过闭包可以创建只允许特定的函数访问变量,而且变量在函数的调用期间依然存在。

var foo = (function () {

    var data = {};

    return function (key, val) {

        if (val == undefined) { // get

            return data[key];

        }

        else { // set

            return data[key] = val;

        }

    }

})();

foo('x');    // x 为 undefined

foo('x', 1); // set

foo('x');    // get

说明:

  • data 是私有变量,从外部不能直接访问 data,它是隐藏的;
  • 创建一个函数,这个函数提供一些访问 data 的方法。

甚至可以写出类似“面向对象”的效果,如下所示:

var Book = function (newIsbn, newTitle, newAuthor) {

    // 私有属性

    var isbn, title, author;

    // 私有方法

    function checkIsbn(isbn) {

        // TODO

    }

 

    // 特权方法

    this.getIsbn = function () {

        return isbn;

    };

 

    this.setIsbn = function (newIsbn) {

        if (!checkIsbn(newIsbn)) throw new Error('Book: Invalid ISBN.');

        isbn = newIsbn;

    };

 

    this.getTitle = function () {

        return title;

    };

 

    this.setTitle = function (newTitle) {

        title = newTitle || 'No title specified.';

    };

 

    this.getAuthor = function () {

        return author;

    };

 

    this.setAuthor = function (newAuthor) {

        author = newAuthor || 'No author specified.';

    };

 

    // 构造器代码

    this.setIsbn(newIsbn);

    this.setTitle(newTitle);

    this.setAuthor(newAuthor);

};

 

// 共有、非特权方法

Book.prototype = {

    display: function () {

        // TODO

    }

};

说明:

  • 用 var 声明变量 isbn、title 和 author,而不是 this,意味着它们只存在 Book 构造器中。checkIsbn 函数也是,因为它们是私有的
  • 访问私有变量和方法的方法只需声明在 Book 中即可。这些方法称为特权方法。因为,它们是公共方法,但却能访问私有变量和私有方法,像 getIsbn、setIsbn、getTitle、setTitle、getAuthor、setAuthor(取值器和构造器)。
  • 为了能在对象外部访问这些特权方法,这些方法前边加了 this 关键字。因为这些方法定义在 Book 构造器的作用域里,所以它们能够访问私有变量 isbn、title 和 author。但在这些特权方法里引用 isbn、title 和 author 变量时,没有使用 this 关键字,而是直接引用。因为它们不是公开的。
  • 任何不需要直接访问私有变量的方法,像 Book.prototype 中声明的,如 display。它不需要直接访问私有变量,而是通过 get*、set* 间接访问。

这种方式创建的对象可以具有真正私有的变量。其他人不能直接访问 Book 对象的任何内部数据,只能通过赋值器。这样一切尽在掌握。但这种方式的缺点是: “门户大开型”对象创建模式中,所有方法都创建在原型 prototype 对象中,因此不管生成多少对象实例,这些方法在内存中只有一份。而采用本节的做法,每生成一个新的对象实例,都将为每个私有方法(如,checkIsbn)和特权方法(如,getIsbn、setIsbn、getTitle、setTitle、getAuthor、setAuthor)生成一个新的副本。

因此,本节方法,只适于用在真正需要私有成员的场合。另外,这种方式也不利于继承。

 

模块化——遍历集合


闭包有益于模块化编程。它能以简单方式开发较小的模块,从而提高开发速度和程序的可复用性。与没有使用闭包的程序相比,使用闭包可将模块划分得更小。例如,计算数组中所有数字的和,只需循环遍历数组相加即可。但现在若要计算所有数字的积?打印所有数字?这些问题都需要遍历数组,若采用闭包,就不得不反复写循环语句,而这在 JavaScript 中就不用。如 jQuery 的 each 方法。

var each = function (object, callback, args) {

 

    //当需要遍历的是一个对象时,name变量用于记录对象的属性名   

    var name;

 

    //当需要遍历的是一个数组时,i变量用于记录循环的数组下标   

    var i = 0;

 

    //遍历数组长度,当需要遍历的对象是一个数组时存储数组长度   

    //如果需要遍历的是一个对象,则length === undefined   

    var length = object.length;

 

    //检查第1个参数object是否是一个对象   

    //根据object.length排除数组类型,根据isFunction排除函数类型(因为函数也是对象)   

    var isObj = length === undefined || typeof (object) == "function";

 

    //回调函数具有附加参数时,执行第一个分支   

    //if(!!args) {   

    if (args) {

 

        //需要遍历的是一个对象   

        if (isObj) {

 

            //遍历对象属性,name是对象的属性名,再函数顶部已声明   

            //许多人不太习惯for(var name in object)方式,如果不进行声明,则name就会被定义为全局变量   

            for (name in object) {

 

                //调用callback回调函数,且回调函数的作用域表示为当前属性的值   

                //如:callback() {  this; //函数中的this指向当前属性值   

                //将each的第3个参数args作为回调函数的附加参数   

                if (callback.apply(object[name], args) === false) {

 

                    //如果在callback回调函数中使用return false;则不执行下一次循环   

                    break;

                }

            }

        }

        //需要遍历的是一个数组   

        else {

 

            //循环长度,循环变量i在函数顶部已定义   

            //循环变量的自增在循环内部执行   

            for (; i < length; ) {

 

                //调用callback函数,与上面注释的callback调用一致   

                //此处callback函数中的this指向当前数组元素   

                if (callback.apply(object[i++], args) === false) {

                    break;

                }

            }

        }

 

    }

    //回调函数没有附加参数时,执行第二个分支   

    else {

 

        //需要遍历的是一个对象   

        if (isObj) {

 

            //循环对象的属性名,name在函数顶部已定义   

            for (name in object) {

 

                //调用callback回调函数   

                //在不带参数的对象遍历中,作用域表示为当前属性的值   

                //且回调函数包含两个参数,第一个数当前属性名,第二个是当前属性值   

                //我觉得这句代码修改一下会更好用:if(callback.call(object, name, object[name]) === false) {   

                if (callback.call(object[name], name, object[name]) === false) {

 

                    //如果在callback回调函数中使用return false;则不执行下一次循环   

                    break;

                }

            }

        }

        //需要遍历的是一个数组   

        else {

            //这里的for写法有点BT,解释为:   

            //var value = object[0];   

            //for(; i < length;) {   

            //    if(false === callback.call(value, i, value)) {   

            //        break;   

            //    }   

            //    value = object[++i];   

            //}   

            //同样,我觉得这里的代码稍加修改会更好用:   

            //for (; i < length && false !== callback.call(object, i, object[i++]);) {   

            //}   

            for (var value = object[0]; i < length && callback.call(value, i, value) !== false; value = object[++i]) {

            }

        }

    }

 

    //这里返回遍历的对象或数组,但object没有被更改,因此一般不给$.each()赋值   

    //但是如果按照我在注释中所修改的写法来使用,且在callback回调函数中对this(即对object的引用)进行了修改   

    //则这里返回的object是被修改后的对象或数组   

    return object;

}

 

var arr = [1, 2, 3, 4, 5];

each(arr, function (index, value) {

    alert(index + ':' + value);

}); 

 

抽象性


从上面可以看到,闭包可以封装数据和行为,具有较好抽象能力,可以用来实现面向对象的委托和接口。

另外,闭包也简化了代码,一个常见问题是,窗口上有个按钮,当点击按钮时会产生事件。如果在按钮中处理这个事件,那就必须在按钮中保存处理这个事件时所需的各个对象的引用。另一个选择是把这个事件转发给父窗口,由父窗口来处理,或使用监听者模式。无论哪种方式,都不太方便,甚至要借助一些工具来帮助生成事件处理的代码框架。用闭包来处理就比较方便了,可以在按钮里直接写事件处理代码。

 

下载 Demo

JavaScript 从闭包可以做什么开始,将有助于理解闭包的更多相关文章

  1. JavaScript——以简单的方式理解闭包

    闭包,在一开始接触JavaScript的时候就听说过.首先明确一点,它理解起来确实不复杂,而且它也非常好用.那我们去理解闭包之前,要有什么基础呢?我个人认为最重要的便是作用域(lexical scop ...

  2. javaScript深入浅出之理解闭包

    javaScript深入浅出之理解闭包 引言 闭包是个老生长谈的话题了,对于闭包网上也有很多不同的看法 <你不知道的javaScript>对于闭包是这么定义的:函数创建和函数执行不在同一个 ...

  3. javascript深入理解闭包

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量 ...

  4. javascript深入理解闭包(转)

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量 ...

  5. 有人说,即使没有JavaScript,你也可以做网页。在纯HTML

    有人说,即使没有JavaScript,你也可以做网页.在纯HTML +服务器端语言理论中也可以完成所有功能,那么,为什么以及在哪里存在JavaScript?   JS,全称JavaScript   在 ...

  6. javascript花式理解闭包

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量 ...

  7. Javascript之深入理解闭包

    闭包算是js里面比较不容易理解的点,尤其是对于没有编程基础的人来说. 其实闭包要注意的就那么几条,如果你都明白了那么征服它并不是什么难事儿.下面就让我们来谈一谈闭包的一些基本原理. 闭包的概念 一个闭 ...

  8. JavaScript要理解闭包先了解词法作用域

    之所以取名叫做词法作用域,是这个概念是js中相当基础也是极为重要的,很多想当然的错误或感觉怪异的问题都是和这个东西有关.所以,本文主要说下这个名词的概念以及讨论下他牵扯出来的有关变量.函数.闭包的问题 ...

  9. 深入理解闭包系列第三篇——IIFE

    × 目录 [1]实现 [2]用途 前面的话 严格来讲,IIFE并不是闭包,因为它并不满足函数成为闭包的三个条件.但一般地,人们认为IIFE就是闭包,毕竟闭包有多个定义.本文将详细介绍IIFE的实现和用 ...

随机推荐

  1. mysql 阿里内核人员

    丁奇 http://dinglin.javaeye.com/ 鸣嵩 @曹伟-鸣嵩 (新浪微博) 彭立勋 http://www.penglixun.com/ 皓庭 http://wqtn22.iteye ...

  2. DELPHI实现关机,兼容全部WINDOWS系统 转

    {=================================================================================================== ...

  3. delphi实现数字签名

    上周,另一部门需要支援解决数字签名问题.但因为之前也没做过,现学现卖.此方面可参考的中文资料较少,特作分享,方便查阅. 上周,另一部门需要支援解决数字签名问题.但因为之前也没做过,现学现卖.此方面可参 ...

  4. Java知识回顾 (2) Java 修饰符

    一.Java 修饰符 1.1 访问控制修饰符 Java中,可以使用访问控制符来保护对类.变量.方法和构造方法的访问.Java 支持 4 种不同的访问权限. default (即缺省,什么也不写): 在 ...

  5. Objective-C市场占有率排名升至第4位

    TIOBE近日公布了2012年4月份的编程语言排行榜,终于不出小编所料,在上个月的编程语言排行榜中说过的“编程语言的王者之争不久很可能会发生改变”实现了,一方面是Java在上几个月中一直属于下滑状态, ...

  6. C#编程(五十)----------栈

    栈 栈与队列是一个非常类似的容器,他们的区别在于队列是先进先出,而栈是后进先出. Stack与Stack<T>,像队列一样,栈也提供了泛型与非泛型版本. Stack的方法: 方法 说明 P ...

  7. spring boot对输入的字符串进行html转码

    可以使用HtmlUtils这个类进行操作.具体的可以参考API,或者点出来看.

  8. [PHP] 6种负载均衡算法

    CP from  : https://www.cnblogs.com/SmartLee/p/5161415.html http://www.dataguru.cn/thread-559329-1-1. ...

  9. 命令行界面 (CLI)、终端 (Terminal)、Shell、TTY的区别

    虽然这个话题已是老生常谈,搜索一下应该也能找到大把的相关文章.不过难得提到了这方面,就趁此机会把我的理解写下来,一来看看我是不是真正理解了,二来看看我能不能把它们之间的区别讲得更加简明易懂. 0. 太 ...

  10. 虚拟机内存复用技术的比较(XEN系统)

    技术途径 业界就该问题定义为虚拟机内存复用(复用干嘛? 当然是为了跑更多的虚拟机呀!) :memory overcommit.围绕次问题主要有4种技术手段,下面简要介绍和分析: 1 气泡驱动(ball ...