• JavaScript高级之闭包的概念及其应用


    主要内容:

    1. 什么是闭包
    2. 闭包使用的一般模式
    3. 闭包都能做些什么


      本文是我的JavaScript高级这个系列中的第二篇文章. 在这个系列中,我计划分析说明 一下JavaScript中的一些常用的而又神秘的高级内容,包括:作用域链、闭包、函数调用形
    式、面向对象等内容. 本文就闭包做个说明. 一说到JavaScript,就能想到闭包是个神奇的东西. 到底闭包是什么,以及怎么使用? 今天我们来分析一下!
      同样,这个也属于JavaScript的高级的部分,对于JavaScript而言基础非常重要,对于 基本语法,动态语言的基本特征希望不太了解的朋友,找本书或一些系统点的资料看看. 这
    样有助于对后文的理解. 当然,也可以到http://net.itcast.cn中去下载一下东西看看.
      下面正式进入今天的主题.

    一、何为闭包

      "闭包"这个词并非是JavaScript特有的,实际上闭包是一个特有的概念. 至于概念本身 我不过多介绍,百度一下什么都有. 我主要说说JavaScript中闭包是什么.
      在JavaScript中闭包就是函数

      闭包就是函数,这个概念似乎感觉有点迷惑. 实际上很简单,闭包就是一个封闭包裹的 范围. 前文咱们提到过,函数可以限定变量的作用域. 一个变量在函数内部声明,那么在函
    数外部是无法访问的. 那么这个就是一个封闭的范围. 广义上说就是一个闭包了!
      那么这个样子其实没有什么意义. 因为没有什么特别的地方, 但是如果函数中又定义了 函数,并将这个函数以返回值的形式返回,那么,在JavaScript中"子域访问父域"的规则就
    会打破了. 因为这个时候,在函数外就可以访问函数内的变量. 看下面代码:

    1 var func = function() {
    2     var num = 10;
    3     return function() {
    4         alert(num);
    5     };
    6 };
    7 var foo = func();
    8 foo();

    这段代码中,函数foo是0级链,而变量num是在1级链中,这个时候,0级链的函数就访问了1级

    链中的变量num,这段代码运行结果是打印出10. 这样就实现了JavaScript中的闭包.

      小结一下,JavaScript中的闭包就是在函数中定义变量,然后通过返回值,将可以访问这 个变量的函数返回,这样在函数外就可以访问函数内的变量了. 这样就形成了闭包.


    二、闭包的使用案例及其说明

      闭包的案例非常的多. 在JavaScript中,使用闭包就像C语言中使用指针一样. 其基本语法 简单,但是使用灵活多变,使用其灵活的语法与特征就能实现许多非常强大的功能. 在此不能阐
    述闭包的所有用法,但是对于刚刚接触闭包的朋友,下面的案例足够理解一段时间了.

    2.1 模拟私有成员

      这个案例是JavaScript实现面向对象的基础. 看下面代码

     1 var Person = function(name, age, gender) {
     2     return {
     3         get_name : function() {
     4                         return name;
     5                 },
     6         set_name :    function(value) {
     7                         name = value;
     8                 },
     9         get_age :    function(){
    10                         return age;
    11                 },
    12         get_gender :    function(){
    13                         return gender;
    14                 }
    15     };
    16 };        

    这段代码就是一个函数,函数带有三个参数,也就是说在函数内部有三个局部变量,分别表示姓

    名(name)、年龄(age)和性别(gender). 然后在返回值中,返回一个对象,该对象提供四个方法.
    分别给年龄提供读写方法,给性别与年龄提供读取的方法. 这四个函数都是这个函数的子域. 因
    此返回的这个对象就可以直接访问这三个变量. 但是有了读写的访问权限的限制.


    2.2 Fibonacci数列

      Fibonacci数列就是:1, 1, 2, 3, 5, 8, 13, ...
      这个案例是面试题中经常考到的案例,也算是具有代表性的算法题. 看下面代码:

     1 // 为了简单就不做n的判断处理了
     2 var Fib = (function() {
     3     var fibArr = [1,1];
     4     return function( n ) {
     5         var res = fibArr[n];
     6         if(res) {
     7             return res;
     8         } else {
     9             res = arguments.callee(n - 1) + arguments.callee(n - 2);
    10             fibArr.push(res);
             // 这里掉了一句代码
             return res;
    11 } 12 }; 13 })();

    这个案例一般传统的做法就是使用递归,但是递归的性能问题十分可怕,如果大家有兴趣可以 计算一下这个数列的第20项结果是多少,并统计一下这个函数递归调用了多少次. 如下面代码

    1 var count = 0;
    2 var fib = function(n) {
    3     count++;
    4     // 为了简单就不做n的判断处理了
    5     if(n == 0 || n == 1) return 1;
    6     return fib(n-1) + fib(n-2);
    7 };
    8 var res = fib(20);
    9 alert("fib(20)的结果为:" + res + ", 函数调用了 " + count + " 次");

    然后再用新方法,计算同样的结果,并统计一下次数. 

     1 var count = 0; // 为了简单就不做n的判断处理了
     2 var Fib = (function() {
     3     var fibArr = [1,1];
     4     return function( n ) {
     5         count++;
     6         var res = fibArr[n];
     7         if(res) {
     8             return res;
     9         } else {
    10             res = arguments.callee(n - 1) + arguments.callee(n - 2);
    11             fibArr.push(res);
    12             return res;
    13         }
    14     };
    15 })();
    16 var res = Fib(20);
    17 alert("Fib(20)的结果为:" + res + ", 函数调用了 " + count + " 次");

    这个结果,我不在这里揭晓,请大家自己下去运行看看.

      下面分析一下这段新方法的代码. 在这段代码中,绑定在Fib中的函数,实际上是后面函数运 行的返回结果. 后面这个函数有一个私有变量,是一个数组. 保存着第0项和第1项数组的值. 然后
    返回一个函数. 在调用 Fib(20) 的时候就是在执行这个被返回的函数.
      这个函数中,首先访问数组的第n项值,如果数组中有这个数据,就直接返回,否则实现递归 计算这个值,并将值加到数组中,最后返回计算的结果. 在JavaScript中,递归使用
    arguments.callee()表示当前调用函数(即递归函数).
      那么这么做最直接的结果是,存在一个缓存,将计算得到的结果保存在缓存中,并且实现所有 的计算只计算一次,那么可以大大的提高性能.


    2.3 html字符串案例

      这个是许多js库使用的办法,在很多js库中需要使用正则表达式处理一些数据,而如果每次执 行都在方法中保存需要处理匹配的字符串,那么会大量的消耗内存,影响性能. 因此可以将重复使
    用的表达式都保存在闭包中,每次使用都是访问的这个字符串. 例如:

     1 String.prototype.deentityify = function() {
     2     var entity = {
     3         lt     :    '<',
     4         gt     :    '>'
     5     };
     6     return function() {
     7         return this.replace(/&([^;]+);/g, function(a,b) {
     8             var r = entity[b]; 
     9             return typeof r === 'string' ? r : a; 
    10         });
    11     };
    12 }();

    这段代码会将任何一个字符串中的 &lt; 和 &gt; 都替换成尖括号<和>,对于页面html代码的复制

    非常好用.

    2.4 事件处理方法的追加与移除

      在JavaScript中并不支持事件处理函数的追加. 大师 Jeremy Keith 给出了一个办法:

     1 var loadEvent = function( fn ) {
     2     var oldFn = window.onload;
     3     if( typeof oldFn === "function" ) {
     4         window.onload = function() {
     5             oldFn();
     6             fn();
     7         };
     8     } else {
     9         window.onload = fn;
    10     }
    11 };

    不过这段代码没有办法移除已经追加的方法,那么使用闭包的缓存功能就可以轻易的实现.

     1 var jkLoad = (function() {
     2     var events = {};
     3     var func = function() {
     4         window.onload = function() {
     5             for(var i in events) {
     6                 events[i]();
     7             }
     8         };
     9     };
    10     return {
    11             add     :    function(name, fn) {
    12                         events[name] = fn;
    13                         func();
    14                 },
    15             remove :    function(name) {
    16                         delete events[name];
    17                         func();
    18                 }
    19         };
    20 })();

    这段代码就是得到用来追加和移出load事件的对象. 如果要追加事件,可以使用

    1 jkLoad.add("f1", function() {
    2     // 执行代码1
    3 });

    如果要移除事件处理函数,就是用代码 

    1 jkLoad.remove("f1");

    那么这个案例还可以扩展到对应以对象追加指定的事件,那么怎么实现,请大家

    自己考虑吧!!!

    三、小结

      到此,我们已经分析了闭包是什么,以及闭包的实现一般方式,最后又分析了 几个闭包的案例. 我想大家应该对闭包有了更为深刻的理解. 那么在后面的面向对
    象等高级内容中,我们将再次看到闭包的强大之处.

    下面对前面问题做个解答:

    第一个问题:

     1 var func = function() {
     2     alert("调用外面的函数");
     3 };
     4 var foo = function() {
     5     func();
     6 
     7     var func = function() {
     8         alert("调用内部的函数");
     9     };
    10 
    11     func();
    12 };

    这段代码在IE下会报错,而在FF和Chrome中会没有任何效果,因为在foo中第一个函

    数的调用func()就会报错,出现异常,因此后面代码不在执行. 如果需要修改,只需
    要try-catch一下就好. 如:

     1 var func = function() {
     2     alert("调用外面的函数");
     3 };
     4 var foo = function() {
     5     try {
     6         func();
     7     } catch ( e ) {
     8         alert( e );
     9     }
    10     var func = function() {
    11         alert("调用内部的函数");
    12     };
    13 
    14     func();
    15 };

    第二个问题:

    1 if(! "a" in window) {
    2     var a = "定义变量";
    3 }
    4 alert(a);

    这段代码会返回 undefined.
    首先,这段代码中没有函数,因此在if中定义的变量会提前,即等价于

    1 var a;
    2 if(! "a" in window) {
    3     var a = "定义变量";
    4 }
    5 alert(a);

    而 in 运算符是用来判断左边的字符串表示的属性是否是右边对象的成员. 在浏览器

    中JavaScript的全局对象就是window,而直接定义的变量实际上就是全局对象的一个
    属性,因此如果已经定义了变量a,那么 "a" in window 就返回true,然后取反,即
    为false,所以if中的代码不会执行,就不会给a赋值,所以打印结果为 undefined.
    上面代码就等价于:

    1 var a;
    2 if( false ) {
    3     a = "定义变量";
    4 }
    5 alert(a);
  • 相关阅读:
    快速排序算法(c#)
    NHibernate 中createSqlQuery的执行
    Asp.net页面下客户端按钮提交页面到其他Action
    希尔(插入)排序 c#代码
    Asp.net MVC 中冒号的作用
    Net注册JS的几种方式和区别
    Asp.net MVC 使用json数据格式交互
    DataSet的手工创建
    反射基础
    uva10082 WERTYU
  • 原文地址:https://www.cnblogs.com/jiangkun86/p/JavaScript_Closure.html
Copyright © 2020-2023  润新知