第七章 函数表达式(二)

2. 模仿块级作用域

前面我们说到,JavaScript中没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。下面是一个例子:

function outputNumber(count){
    for (var i=0; i<count; i++){
        alert(i);
    }
    alert(i);
}

上面的代码是完全可执行的,因为在for语句中定义的变量i是出于函数作用域的,这也说明了JavaScript中没有块级作用域。另外,JavaScript从来不会在意是否多次声明了同一个变量,如果遇到这种情况(例如在for循环结束后,使用var i;再次定义一个i变量,也不会改变i的值,除非你在定义时显示的初始化了),它只会直接忽略后续的声明。

我们可以通过匿名函数来模拟块级作用域。下面是一个例子:

function outputNumber(count){
    (function(){
         for (var i=0; i<count; i++){
            alert(i);
         }
    })();
    alert(i);  //error!
}

在这个重写后的 outputNumbers()函数中,我们在 for 循环外部插入了一个私有作用域。在匿名 函数中定义的任何变量,都会在执行结束时被销毁。因此,变量 i 只能在循环中使用,使用后即被销毁。 而在私有作用域中能够访问变量 count,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的 所有变量。

3. 私有变量

严格来说,JavaScript中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念:任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数参数,局部变量和函数内部定义的其他函数。下面是一个例子:

function MyObject(){
    //私有变量
    var privateVariable  = 10;
    
    //私有函数
    function privateFunction(){
       return false;
    }

    this.publicMethod = function(){
        privateVariable  ++;
        return privateFunction();
    }
}

我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。上面的例子定义了一个MyObject引用类型,在构造函数内部定义了所有私有变量和函数。然后,有定义了一个名为publicMethod()的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的 所有变量和函数。在创建 MyObject 的实例后,除了使用 publicMethod()这一个途径外,没有任何办法可以直接访问 privateVariable 和 privateFunction()

函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。第 6章曾经讨论 过,构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方 法就可以避免这个问题。

3.1 静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。下面是一个例子:

(function(){
     //私有变量
    var privateVariable  = 10;
    
    //私有函数
    function privateFunction(){
       return false;
    }
    
    //构造函数
    MyObject = function(){
    };
    //特权方法
    MyObject.prototype.publicMethod = function(){
        privateVariable  ++;
        return privateFunction();
    }
})();

这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。公有方法是在原型上定义的, 这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是 使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没 有在声明 MyObject 时使用 var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。 因此,MyObject 就成了一个全局变量,能够在私有作用域之外被访问到。
但也要知道,在严格模式下给未经声明的变量赋值会导致错误。

以这种方式创建静态私有变量会因为使用原型模式而增进代码复用(函数是在对象实例间共享的),但缺点是每个实例都没有自己的私有变量(属性也在实例间共享,因此属性全部都是静态属性)。到底是使用实例变量,还是静态私有变量,终还是要视你的具体需求而定。

3.2 模块模式

前面介绍的模式都是用于为自定义类型创建私有变量和特权方法的,而模块模式主要用于为单例创建私有变量和特权方法。按照惯例,JavaScript是通过字面量来创建单例对象的:

var singleton = {
    name: "Ivan",
    getName: function(){
        return this.name;  
    }
}

使用字面量方式创建的单例对象,其属性都是公开的。因此就有了模块模式:

var singleton = function(){
    //私有变量
    var privateVariable  = 10;
    
    //私有函数
    function privateFunction(){
       return false;
    }

    return {
        publicMethod: function(){
            privateVariable++;
            return privateFunction();
        }
    }
}();

模块模式使用一个匿名函数返回一个对象。字面量对象中定义了公开属性和特权方法。这种模式在需要对单例进行某些初始化,同时又需要维护其私有 变量时是非常有用的,例如:

var application = function(){ 
   //私有变量和函数
   var components = new Array(); 

   //初始化
  components.push(new BaseComponent()); 

   //公共
   return {         
      getComponentCount : function(){
          return components.length;         
      }, 
      registerComponent : function(component){
          if (typeof component == "object"){                 
              components.push(component);             
          }         
       }     
    }; 
}(); 

有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那 些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。来看下面 的例子:

var application = function(){ 
    //私有变量和函数     
    var components = new Array(); 
 
    //初始化 
   components.push(new BaseComponent()); 
 
    //创建application 的一个局部副本 
   var app = new BaseComponent(); 
 
    //公共接口
    app.getComponentCount = function(){
        return components.length;
    }; 
 
    app.registerComponent = function(component){         
        if (typeof component == "object"){             
           components.push(component);        
        }    
     }; 
 
    //返回这个副本 
    return app;
}();

在这个重写后的应用程序(application)单例中,首先也是像前面例子中一样定义了私有变量。主 要的不同之处在于命名变量 app 的创建过程,因为它必须是 BaseComponent 的实例。这个实例实际上 是 application 对象的局部变量版。此后,我们又为 app 对象添加了能够访问私有变量的公有方法。 后一步是返回 app 对象,结果仍然是将它赋值给全局变量 application。

4. 总结

在JavaScript中,函数表达式是一种非常重要的技术,使用函数表达式无需对函数进行命名,从而实现动态编程。下面是函数表达式的一些显著特点:

  • 函数声明要求一定具有名字,而函数表达式则不需要。没有名字的函数表达式也被称为匿名函数。

当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数中的所有变量,原理如下;

  • 闭包函数的作用域链包含着自己的活动对象,包含函数的变量对象以及全局函数的变量对象。因此能够沿着作用域链访问包含函数中的变量。
  • 通常,函数的作用域及其所有变量都将在函数执行完毕后被销毁。
  • 但是当函数返回一个闭包时,这个函数的作用域会一直存在直到闭包被销毁为止。

使用闭包可以在JavaScript中模仿块级作用域,要点如下:

  • 定义并立即调用一个函数,这样即可以执行其中的代码,又不会在内存中留下该函数的引用。

闭包还可以用于在对象中创建私有变量,相关要点如下:

  • 即使 JavaScript中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公 有方法可以访问在包含作用域中定义的变量。
  • 有权访问私有变量的公有方法叫做特权方法。
  • 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块 模式、增强 的模块模式来实现单例的特权方法。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容

  • 本章内容 函数表达式的特征 使用函数实现递归 使用闭包定义私有变量 第五章曾介绍过,定义函数的方式有两种:一种是函...
    Annie_d04e阅读 258评论 0 1
  • 函数表达式是 JavaScript中的一个既强大又容易令人困惑的特性。第 5章曾介绍过,定义函数的 方式有两种:一...
    Xyaleo阅读 192评论 0 1
  • 1.定义函数的方式: ①函数声明(其重要特征是函数声明提升,可以把函数声明放在调用它的语句后面): functio...
    张果果阅读 222评论 0 0
  • 定义函数的两种方式:函数声明和函数表达式。函数声明: sayHi();function sayHi() { al...
    Allenevil阅读 172评论 0 0
  • 函数变量提升:在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。 函数声明: 函数表达...
    简默丶XS阅读 599评论 0 1