天天看點

JavaScript中的塊級作用域和私有變量

模仿塊級作用域:

JavaScript沒有塊級作用域的概念。這意味着在塊語句中定義的變量,實際上是在包含函數中而非語句中建立的。

看一個例子:

 function outputNumbers(count){

         for(vari=0;i<count;i++){

           alert(i);

}

alert(i); //計數

}

這個函數中定義了一個for循環,變量i的初始值被設定為0。在java、C++等語言中,變量i隻會在for循環的語句塊中有定義,循環一旦結束,變量i就會被銷毀。但是在JavaScript中,變量i是定義在outputNumbers()的活動對象中的,是以從它有定義開始,就可以在函數内部随處通路它。即使像下面這樣錯誤的重新聲明同一個變量,也不會改變它的值:

function outputNumbers(count){

         for(vari=0;i<count;i++){

           alert(i);

}

var i; //重新聲明變量

alert(i); //計數

}

JavaScript從來不會告訴你是否多次聲明了同一個變量;遇到這種情況,它隻會對後續的聲明視而不見(不過,它會執行後續聲明中的變量初始化)。

匿名函數可以用來模仿塊級作用域并避免這個問題。

用作塊級作用域(通常稱為私有作用域)的匿名函數的文法如下:

(function(){

  //這裡是塊級作用域

})();

以上代碼定義并立即調用了一個匿名函數。将函數聲明包含在一對圓括号中,表示它實際上是一個函數表達式。而緊随其後的另一對圓括号會立即調用這個函數。

這種不太好了解的文法的由來:

下面的代碼會導緻錯誤:

function(){

  //這裡是塊級作用域

}(); //出錯!

這段代碼會導緻文法錯誤,是因為JavaScript将function關鍵字當做一個函數聲明的開始,而函數聲明後面不能跟圓括号。然而,函數表達式的後面可以跟圓括号。要将函數聲明轉換成函數表達式,隻要像下面這樣給它加上一對圓括号即可:

(function(){

  //這裡是塊級作用域

})();

無論在什麼地方,隻要臨時需要一些變量,就可以使用私有作用域:

function outputNumbers(count){

(function(){

for(var i=0;i<count;i++){

         alert(i);

}

   })();

alert(i); //導緻錯誤!

}

在這個重寫後的outputNumbers()函數中,我們在for循環外部插入了一個私有作用域。在匿名函數中定義的任何變量,都會在執行結束時被銷毀。是以,變量i隻能在循環中使用,使用後即被銷毀。而在私有作用域中能夠通路變量count,是因為這個匿名函數是一個閉包,它能夠通路包含作用域中的所有變量。

這種技術經常在全局作用域中被用在函數外部,進而限制向全局作用域中添加過多的變量和函數。

私有變量:

嚴格來說,JavaScript中沒有私有成員的概念,所有對象屬性都是共有的。不過倒是有私有變量的概念。

任何在函數中定義的變量,都可以認為是私有變量,因為不能在函數的外部通路這些變量。私有變量包括函數的參數、局部變量和在函數内部定義的其他函數。

看一個例子:

function add(num1,num2){

  var sum=num1+num2;

     return sum;

}

在這個函數内部,有3個私有變量:num1、num2和sum。在函數内部可以通路這幾個變量,但在函數外部則不能通路它們。如果在這個函數内部建立一個閉包,那麼閉包通過自己的作用域鍊也可以通路這些變量。利用這一點,就可以建立用于通路私有變量的共有方法。

把有權通路私有變量和私有函數的公有方法稱為特權方法。

下面是第一種在構造函數中定義特權方法的模式:

function MyObject(){

     //私有變量和私有函數

     var privateVariable=10;

     functionprivateFunction(){

         return false;

}

//特權方法

this.publicMethod=function(){

         privateVariable++;

         returnprivateFunction();

};

}

這個模式在構造函數内部定義了所有私有變量和函數。然後,又繼續建立了能夠通路這些私有成員的特權方法。能夠在構造函數中定義特權方法,是因為特權方法作為閉包有權通路在構造函數中定義的所有變量和函數。對這個例子而言,變量privateVariable和函數privateFunction()隻能通過特權方法publicMethod()來通路。在建立MyObject的執行個體後,除了使用publicMethod()這一個途徑外,沒有任何辦法可以直接通路變量privateVariable和函數privateFunction()。

利用私有和特權成員,可以隐藏那些不應該被直接修改的資料:

function Person(name){

     this.getName=function(){

     return name;

};

this.setName=function(value){

         name=value;

};

}

var person=new Person(“Nicholas”);

alert(person.getName()); //“Nicholas”

person.SetName(“Greg”);

alert(person.getName()); //“Greg”

以上代碼的構造函數中定義了兩個特權方法:getName()和SetName()。這兩個方法都可以在構造函數外部使用,而且都有權通路私有變量name。但在Person構造函數外部,沒有任何辦法通路name。由于這兩個方法是在構造函數内部定義的,它們作為閉包能夠通過作用域鍊通路name。私有變量name在Person的每一個執行個體中都不相同,因為每次調用構造函數都會重新建立這兩個方法。

在構造函數中定義特權方法有一個缺點,那就是你必須使用構造函數模式來達到這個目的。

靜态私有變量:

第二種建立特權方法的方法是,通過在私有作用域中定義私有變量或函數:

(function (){

     //私有變量和私有函數

     var privateVariable=10;

     functionprivateFunction(){

         return false;

}

//構造函數

MyObject=function(){

};

//公有/特權方法

MyObject.prototype.publicMethod=function(){

         privateVariable++;

         returnprivateFunction();

};

})();

這個模式建立了一個私有作用域,并在其中封裝了一個構造函數及相應的方法。在私有作用域中,首先定義了私有變量和私有函數,然後又定義了構造函數及其公有方法。公有方法是在原型上定義的,這一點展現了典型的原型模式。需要注意的是,這個模式在定義構造函數時并沒有使用函數聲明,而是使用了函數表達式。函數聲明隻能建立局部函數,但那并不是我們想要的。出于同樣的原因,我們也沒有在聲明MyObject時使用關鍵字var。(記住,初始化未經聲明的變量,總是會建立一個全局變量。)是以,MyObject就成了一個全局變量,能夠在私有作用域之外被通路到。(但也要知道,在嚴格模式下給未經聲明的變量指派會導緻錯誤。)

這個模式與在構造函數中定義特權方法的主要差別,就在于私有變量和函數是由執行個體共享的。由于特權方法是在原型上定義的,是以所有執行個體都使用同一個函數。而這個特權方法,作為一個閉包,總是儲存着對包含作用域的引用。

(function (){

     var name=””;

Person=function(value){

name=value;

};

Person.prototype.getName=function(){

         return name;

};

Person.prototype.setName=function(value){

         name=value;

};

  })();

 var person1=newPerson(“Nicholas”);

alert(person1.getName()); //“Nicholas”

person1.setName(“Greg”);

alert(person1.getName()); //“Greg”

var person2=new Person(“Michael”);

alert(person1.getName()); //“Michael”

alert(person2.getName()); //“Michael”

這個例子中的Person構造函數與getName()和setName()方法一樣,都有權通路私有變量name。在這種模式下,變量name就變成了一個靜态的、由所有執行個體共享的屬性。也就是說,在一個執行個體上調用setName()會影響所有執行個體。而調用setName()或建立一個Person執行個體都會賦予name屬性一個新值。結果就是所有執行個體都會傳回相同的值。以這種方式建立靜态私有變量會因為使用原型而增進代碼複用,但每個執行個體都沒有自己的私有變量。

子產品模式:

(前面的模式是用于為自定義類型建立私有變量和特權方法的。)而子產品模式則是為單例建立私有變量和特權方法。所謂單例,指的就是隻有一個執行個體的對象。按照慣例,JavaScript是以對象字面量的方式來建立單例對象的:

  varsingleton={

         name:value;

         method:function(){

         //這裡是方法的代碼

}

};

子產品模式通過為單例添加私有變量和特權方法能夠使其得到增強,其文法形式如下:

varsingleton=function(){

     //私有變量和私有函數

var privateVariable=10;

     functionprivateFunction(){

         return false;

}

//特權/公有方法和屬性

return{

         publicProperty:true;

publicMethod:function(){

                  privateVariable++;

                  return privateFunction();

}

};

}();

這個子產品模式使用了一個傳回對象的匿名函數,在這個匿名函數内部,首先定義了私有變量和函數,然後,将一個對象字面量作為函數的值傳回。傳回的對象字面量中隻包含可以公開的屬性和方法。由于這個對象是在匿名函數内部定義的,是以它的公有方法有權通路私有變量和函數。從本質上講,這個對象字面量定義的是單例的公共接口。這種模式在需要對單例進行某些初始化,同時又需要維護其私有變量時是非常有用的。

下面看一個例子:

var application=function(){

     //私有變量和函數

     var components=newArray();

     //初始化

     components.push(newBaseComponent());

     //公共

     return{

         getComponentCount:function(){

         returncomponents.length;

},

registerComponent:function(component){

         if(typeof component==”object”){

         components.push(component);

}

}

};

}();

這個簡單的例子建立了一個用于管理元件的application對象。在建立這個對象的過程中,首先聲明了一個私有的components數組,并向數組中添加了一個BaseComponent的新執行個體(在這裡不需要關系BaseComponent的代碼,我們隻是用它來展示初始化操作)。而傳回對象的getComponentCount()和registerComponent()方法,都是有權通路數組components的特權方法,前者隻是傳回已注冊的元件數目,後者用于注冊新組建。

簡單的說就是,如果必須建立一個對象并以某些資料對其進行初始化,同時還要公開一些能夠通路這些私有資料的方法,那麼就可以使用子產品模式。以這種模式建立的每個單例都是Object的執行個體,因為最終要通過一個對象字面量來表示它。

增強的子產品模式:

進一步改進子產品模式,即在傳回對象之前加入對其增強的代碼。這種增強的子產品模式适合那些單例必須是某種類型的執行個體,同時還必須添加某些屬性和(或)方法對其加以增強的情況。

來看一個例子:

var singleton=function(){

     //私有變量和私有函數

var privateVariable=10;

     functionprivateFunction(){

         return false;

}

//建立對象

var object=new CustomType();

//添加特權/公有屬性和方法

object.publicProperty=true;

object.publicMethod=function(){

         privateVariable++;

         return privateFunction();

};

return object;

}();

如果前面示範子產品模式的例子中的application對象必須是BaseComponent的執行個體,那麼就可以使用以下代碼:

var application=function(){

     //私有變量和函數

     var components=newArray();

     //初始化

     components.push(newBaseComponent());

     //建立application的一個局部副本

     var app=new BaseComponent();

     //公共接口

app.getComponentCount=function(){

return components.length;

},

app.registerComponent=function(component){

if(typeof component==”object”){

components.push(component);

}

};

         //傳回這個副本

         retutnapp;

}();

在這個重寫後的application單例中,首先也是像前面例子中一樣定義了私有變量。主要的不同之處在于命名變量app的建立過程,因為它必須是BaseComponent的執行個體。這個執行個體實際上是application對象的局部變量版。此後,我們又為app對象添加了能夠通路私有變量的公有方法。最後一步是傳回app對象,結果仍然是将它指派給全局變量application。

繼續閱讀