《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));
好了,這一篇就到這裡了,上訴的程式碼,實用價值是很大的。希望大家可以仔細閱讀,認真看看。嘿