本文内容

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

闭包是 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. AIX 与Linux 中crontab 介绍

    AIX 与Linux 中crontab 用法相似,先介绍Linux 中的Crontab 用法,再后介绍AIX 与Linux 的不同之处.   一.Crontab 介绍 crontab命令的功能是在一定 ...

  2. nil coalescing operator

    nil coalescing operator ?? 就是 optional和 三元运算符?:的简写形式. 比如一个optional String类型的变量 var a:String? // prin ...

  3. 委托、Lambda表达式、事件系列07,使用EventHandler委托

    谈到事件注册,EventHandler是最常用的. EventHandler是一个委托,接收2个形参.sender是指事件的发起者,e代表事件参数. □ 使用EventHandler实现猜拳游戏 使用 ...

  4. 推荐Java基础

    (一) 基础篇 01. Java多线程系列--“基础篇”01之 基本概念 02. Java多线程系列--“基础篇”02之 常用的实现多线程的两种方式 03. Java多线程系列--“基础篇”03之 T ...

  5. 解决Installation error: INSTALL_FAILED_VERSION_DOWNGRADE错误

    Installation error: INSTALL_FAILED_VERSION_DOWNGRADE 说明你手机里已经装的软件版本比你要安装的软件版本要高,所以不能安装. 你只要删除你安装的应用便 ...

  6. 开源项目MultiChoiceAdapter详解(五)——可扩展的MultiChoiceBaseAdapter

    上次写到了开源项目MultiChoiceAdapter详解(四)——MultiChoiceBaseAdapter的使用,其实我们仍旧可以不使用ActionMode的,所以这里就写一个自己扩展的方法. ...

  7. 低版本系统兼容的ActionBar(二)ActionProvider+分离式ActionBar+分离式的ActionMode

           这篇文章主要讲的是在低版本兼容的ActionBar中实现自定义的ActionProvider,ShareActionProvider的使用方法,如何实现分离式ActionBar,外加在分 ...

  8. C#和java之间的一些差异与共性

    C#与java之间的一些共性和差异整理 隐藏:与java中的重写几乎一致,但是需要添加new关键字让编译器知道,否则会有警告 虚方法:1.声明为virtual的方法就是虚方法,在子类中使用overri ...

  9. Linux常用命令汇总 - Linux Shell Cheat Sheet

    1. 查看Linux操作系统信息: uname -a cat /proc/version lsb_release -a 2. 设置ls显示的文件夹的颜色(将下面这条目录加在 .bashrc 文件最后) ...

  10. window.parent window.top window.parent.location.pathname 没权限

    跨域问题啊,如果只是测试的话,放到服务器去测试,本地的(http://localhost/)就算是在同一个文件下,都会被认为跨域的.如果不需要支持低版本IE浏览,可以使用postMessage处理这个 ...