《JavaScript 模式》讀書筆記(5)— 對象創建模式2

  這一篇,我們主要來學習一下私有屬性和方法以及模組模式。

 

三、私有屬性和方法

  JavaScript並沒有特殊的語法來表示私有、保護、或公共屬性和方法,在這一點上與Java或其他語言是不同的。JavaScript中所有對象的成員是公共的:

var myobj = {      myprop:1,      getProp: function() {          return this.myprop;      }  };  console.log(myobj.myprop); // 'myprop'是共有可訪問的  console.log(myobj.getProp()); //getProp()也是公有可訪問的    // 當使用構造函數創建對象時也通用如此,即所有的成員仍然都是公共的:  function Gadget() {      this.name = 'iPod';      this.stretch = function () {          return 'iPad';      };  }  var toy = new Gadget();  console.log(toy.name); // 'name'是共有的  console.log(toy.stretch()); //stretch()是公有的

  

私有成員

  雖然JavaScript語言中並沒有用於私有成員的特殊語法,但是可以使用閉包來實現這種功能。構造函數創建了一個閉包,而在閉包範圍內部的任意變數都不會暴露給構造函數以外的程式碼。然而,這些私有變數仍然可以用於公共方法中:即定義在構造函數中,且作為返回對象的一個部分暴露給外部的方法。

  我們來看個例子,其中name是一個私有成員,在構造函數外部是不可訪問的:

function Gadge() {      // 私有成員      var name = 'iPod';      // 公有函數      this.getName = function () {          return name;      };  }  var toy = new Gadge();    // 'name'是undefined的,它是私有的  console.log(toy.name);//undefined    // 公有方法訪問'name'  console.log(toy.getName());// 'iPod'

  正如所看到的,很容易在JavaScript中實現私有性。需要做的只是在函數中將需要保持為私有屬性的數據包裝起來,並確保它對函數來說是局部變數,這意味著外部函數不能訪問它。

 

特權方法

  特權方法(Privileged Method)的概念並不涉及任何特殊語法,它只是指那些可以訪問私有成員的公共方法(因此它擁有更多的特權)的一個名稱而已。

  在前面的例子中,getName()就是一個特權方法,它具有訪問私有屬性name的“特殊”許可權。

 

私有性失效

  當關注私有的時候就會出現一些邊緣情況:

  • 舊版本瀏覽器的一些情況比如Firefox的eval()可以傳遞第二個上下文參數,比如Mozilla的__parent__屬性也與此類似。但是這幾乎都是在古代瀏覽器才存在,現代瀏覽器幾乎已經不存在這種情況了。
  • 當直接從一個特權方法中返回一個私有變數,且該變數恰好是一個對象或者數組,那麼外面的程式碼仍然可以訪問該私有變數,這是因為它是通過引用傳遞的。

  我們來看下這種情況。以下Gadget的實現看起來就像是無意造成失效的:

function Gadget() {      // 私有成員      var specs = {          screen_width:320,          screen_height:480,          color:"white"      };      // 公有函數      this.getSpecs = function () {          return specs;      };  }    // 這裡的問題是在於getSpecs()方法返回了一個引用的specs對象。這使得Gadget的用戶可以修改表面上看起來是隱藏和私有的specs對象:  var toy = new Gadget(),      specs = toy.getSpecs();  specs.color = "black";  specs.price = "free";    console.dir(toy.getSpecs());    

  對於這種意外行為的解決方法是保持細心,既不要傳遞需要保持私有性的對象和數組的引用。解決這個問題的一種方法是,使getSpecs()返回一個新對象,而該對象僅包含客戶關注的原對象中的數據。這也是眾所周知的最低授權原則(Principle of Least Authority,POLA),其中規定了應該永遠不要給予超過需要的特權。

  在這種情況下,如果Gadget的消費者僅關注該gadget組建是否與一個特定方框的尺寸相符合,那麼它需要的僅是尺寸規格。因此,並不需要分發所有的數據,可以創建getDimensions()使其返回一個包含寬度和高度的新對象。此時,可能根本不需要實現getSpecs()。

  當需要傳遞所有的數據時,另外一種解決方法是使用一個通用性的對象克隆(object cloning)函數以創建specs對象副本。下一章提供了兩個這樣的函數,其中一個名為extend(),它可以針對給定對象創建一個淺複製(shallow copy)副本(僅複製頂級單數)。而另一個名為extendDeep()的函數,它可以通過遞歸複製所有的屬性以及其嵌套屬性而創建深度複製(deep copy)副本。

 

對象字面量以及私有性

  到目前為止,我們僅看到了使用構造函數獲得私有性的例子。但是當使用對象字面量(object literal)來創建對象會是什麼情況?他是否還有可能擁有私有成員?

  正如在前面所看到的,需要的只是一個能夠包裝私有數據的函數。因此,在使用對象字面量的情況下,可以使用一個額外的匿名即時函數(anonymous immediate function)創建閉包來實現私有性:

var myobj; //這將會是對象  (function() {      // 私有成員      var name = 'my,oh my';        // 實現公有部分      // 注意,沒有'var'修飾符      myobj = {          // 特權方法          getName: function () {              return name;          }      };  }());  myobj.getName(); //'my, oh my';    // 下面的例子與上面的具有同樣的思想,但是在實現上略有不同:  var myobj = (function () {      // 私有成員      var name = 'my,oh my';        // 實現公有部分      return {          getName:function () {              return name;          }      };  }());  // 這個例子也是模組模式的基礎框架,後面會再聊。

 

原型和私有性

  當將私有成員與構造函數一起使用時,其中有一個缺點在於每次調用構造函數以創建對象時,這些私有成員都會被重新創建。構造函數中添加到this中的任何成員實際上都面臨以上問題。為了避免複製工作以及節省記憶體,可以將重用屬性和方法添加到構造函數的prototype屬性中。這樣,通過同一個構造函數創建的多個實例可以共享常見的部分數據。此外,還可以再多個實例中共享隱藏的私有成員。為了實現這一點,可以使用以下兩個模式的組合:即構造函數中的私有屬性以及對象字面了中的私有屬性。由於prototype屬性僅是一個對象,因此可以使用對象字面了創建該對象。

function Gadget() {      // 私有成員      var name = 'iPod';      // 公有函數      this.getName = function () {          return name;      };  }    Gadget.prototype = (function () {      // 私有成員      var browser = "Mobile Webkit";      // 公有原型成員      return {          getBrowser: function() {              return browser;          }      };  }());    var toy = new Gadget();  console.log(toy.getName()); //自身特權方法  console.log(toy.getBrowser());// 原型特權方法

 

將私有方法揭示為公共方法

  揭示模式(revelation pattern)可用於將私有方法暴露成為公共方法。當為了對象的運轉而將所有功能都放置在一個對象中,以及,想儘可能的保護該對象的時候,這種揭示模式就顯得非常有用。不過,同時可能也想為其中的一些功能提供公共可訪問的介面,因為那可能也是有用的。當這些私有方法暴露為公共方法時,也使他們變得更為脆弱。因為使用公共API的一些用戶可能會修改原對象,甚至是無意的修改。在ES5中,可以選擇將一個對象凍結,但是在前一版本的語言中是不具備該功能的。

  揭示模式的前提,是建立在對象字面量的私有成員之下的。

var myarray;  (function () {      var astr = "[Object Array]",          toString = Object.prototype.toString;        function isArray(a) {          return toString.call(a) === astr;      }        function indexOf(haystack,needle) {          var i = 0,              max = haystack.length;            for(;i < max; i += 1) {              if(haystack[i] === needle) {                  return i;              }          }          return -1;      }      myarray = {          isArray:isArray,          indexOf:indexOf,          inArray:indexOf      }  }());    // 上面的例子中,有兩個私有變數以及兩個私有函數,isArray()和indexOf()。  // 在匿名函數(immediate function)的最後,對象myarray中填充了認為適用於公共訪問的功能。  // 在這種情況下,同一個私有函數indexOf()可以暴露為ES5風格的indexOf以及PHP範式的inArray。  myarray.isArray([1,2]); // true  myarray.isArray({0:1}); // false  myarray.indexOf(["a","b","z"],"z"); // 2  myarray.inArray(["a","b","z"],"z"); // 2    // 現在,如果發生了意外的情況,例如公共indexOf()方法發生意外,但私有indexOf()方法仍然是安全的,因此inArray()將繼續正常運行:  myarray.indexOf = null;  myarray.inArray(["a","b","z"],"z"); // 2

 

四、模組模式

  目前模組模式得到了廣泛的應用,因為它提供了結構化的思想並且有助於組織日益增長的程式碼。與其他語言不同的是,JavaScript並沒有(package)的特殊語法,但是模組模式提供了一種創建自包含非耦合(self-contained de-coupled)程式碼片段的有利工具,可以將它視為黑盒功能,並且可以根據您所編寫軟體的需求添加、替換或刪除這些模組。

  模組模式是本系列中迄今為止介紹過的第一種多種模式組合的模式,也就是以下模式的組合:命名空間、即時函數、私有和特權成員、聲明依賴。

  該模式的第一步時間裡一個命名空間。讓我們使用本章前面介紹的namespace()函數,並且啟動可以提供有用數組方法的工具模組。

MYAPP.namespace('MYAPP.utilities.array');  // 下一步是定義該模組。對於需要保持私有性的情況,本模式使用了一個可以提供私有作用域的即時函數。  // 該即時函數返回了一個對象,即具有公共介面的實際模組,可以通過這些介面來使用這些模組。  MYAPP.utilities.array = (function () {      return {          // todo...      }  }());    // 接下來我們向該公共介面添加一些方法:  MYAPP.utilities.array = (function () {      return {          inArray:function(needle,haystack) {              // ...          },          isArray:function(a) {              // ...          }      };  }());

  通過使用由即時函數提供的私有作用域,可以根據需要聲明一些私有屬性和方法。在即時函數的頂部,正好也就是聲明模組可能由任何依賴的為止。在變數聲明之後,可以任意地放置有助於建立該模組的任何一次性的初始化程式碼。最終結果是一個由即時函數返回的對象,其中該對象包含了您模組的公共API:

MYAPP.namespace('MYAPP.utilities.array');    // 接下來我們向該公共介面添加一些方法:  MYAPP.utilities.array = (function () {      // 依賴      var uobj = MYAPP.utilities.object,          ulang = MYAPP.utilities.lang,      // 私有屬性      array_string = "[Object Array]",      ops = Object.prototype.toString;        // 私有方法      // ...        // var 變數定義結束        // 可選的一次性初始化過程      // ...        return {          inArray:function(needle,haystack) {              for(var i = 0;i < haystack.length; i += 1) {                  if(haystack[i] === needle) {                      return true;                  }              }          },          isArray:function(a) {              return ops.call(a) === array_string;          }          // 更多方法和屬性      };  }());

  模組模式得到了廣泛的使用,並且強烈建議使用這種方式組織您的程式碼,尤其是當舊程式碼日益增長的時候。

 

揭示模組模式

  我們已經討論了揭示模式,同時還考慮了私有模式。模組模式也可以組織成與之相似的方式,其中所有的方法都需要保持私有性,並且只能暴露那些最後決定設立API的那些方法。根據這些思想,程式碼是這樣的:

MYAPP.namespace('MYAPP.utilities.array');    MYAPP.utilities.array = (function () {      // 私有屬性      var array_string = "[Object Array]",      ops = Object.prototype.toString,      // 私有方法      inArray = function (needle,haystack) {          for(var i = 0;i < haystack.length; i += 1) {              if(haystack[i] === needle) {                  return true;              }          }          return -1;      },      isArray = function(a) {          return ops.call(a) === array_string;      }      // var 變數定義結束        // 揭示公有API      return {          isArray:isArray,          indexOf:inArray      };  }());

 

創建構造函數的模組

  前面的例子中創建了一個對象MYAPP.utilities.array,但有時候使用構造函數創建對象更為方便。當然,可以仍然使用模組模式來執行創建對象的操作。它們之間唯一的區別在於包裝了模組的即時函數最終將會返回一個函數,而不是返回一個對象。

  考慮以下使用模組模式的例子,在該例子中創建了一個構造函數MYAPP.utilities.Array:

MYAPP.namespace('MYAPP.utilities.Array');    MYAPP.utilities.Array = (function () {      // 依賴      var uobj = MYAPP.utilities.object,          ulang = MYAPP.utilities.lang,        // 私有屬性和方法      Constr;        // var 變數定義結束        // 可選的一次性初始化過程      // ...        // 公有API——構造函數      Constr = function(o) {          this.elements = this.toArray(o);      };      // 公有API——原型      Constr.prototype = {          constructor:MYAPP.utilities.Array,          version:"2.0",          toArray:function(obj) {              for(var i = 0,a = [],len = obj.length; i < len; i += 1) {                  a[i] = obj[i]              }              return a;          }      };        // 返回要分配給新命名空間的構造函數      return Constr;  }());    // 這樣使用  var arr = new MYAPP.utilities.Array(obj);

  

將全局變數導入到模組中

  在常見的變化模式中,可以將參數傳遞到包裝了模組的即時函數中。可以傳遞任何值,但是通常這些都是對全局變數、甚至是全局對象本身的引用。導入全局變數有助於加速即時函數中的全局符號解析的速度,因為這些導入的變數成為了該函數的局部變數。

MYAPP.utilities.module = (function(app,global) {      // 引用全局對象      // 以及現在被轉換成局部變數的全局應用程式命名空間對象  }(MYAPP,this));

 

  好了,這一篇就到這裡了,上訴的程式碼,實用價值是很大的。希望大家可以仔細閱讀,認真看看。嘿嘿。