前言
也算老生常談的問題了,再深入搞一搞怎么玩兒封裝,如果看到這篇文章的你,正好你也是追求完美的代碼潔癖狂者,那么這篇文章相信非常適合你。
舉一個例子,編寫一個Person類,具有name和birthday(時間戳)兩個屬性及對應的getter和setter方法,注意,setBirthday輸入的參數是日期字符串,如"2016-04-08"。getBirthday同樣得到的也是日期字符串。那么這個類是這樣的——
var Person = function(name, birthday) { this.name = name; this.birthday = birthday; // timestamp }; function getTimestampOfInput(dateString) { return new Date(dateString).getTime(); } function getFormattedDay(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } Person.prototype = { setName: function(name) { this.name = name; }, getName: function() { return this.name; }, /** * 設置生日 * @param dateString */ setBirthday: function(dateString) { this.birthday = getTimestampOfInput(dateString); }, /** * 獲取生日 * @returns {*} */ getBirthday: function() { return getFormattedDay(this.birthday); } };
如果采用面向過程的方式去寫,我們需要借助自執行匿名函數閉包的方式,如——
// 常用模式一:單例/靜態 - 私有變量&共有方法 // 生成一個人 var person = (function() { // 私有變量 var name = ''; var birthday = new Date().getTime(); // 默認是時間戳方式 // 共有方法 return { setName: function(newName) { name = newName; }, getName: function() { return name; }, setBirthday: function(dateString) { // 私有函數 function getTimestampOfInput() { return new Date(dateString).getTime(); } birthday = getTimestampOfInput(); }, getBirthday: function() { return getFormattedDay(birthday); // 函數式 - 不訪問外界變量,沒有閉包的呈現 // 有了輸入,便有了預想中的輸出,不保存狀態 // 私有函數 - 已工具方法存在 function getFormattedDay(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } } }; })(); person.setName('king'); console.log(person.getName()); person.setBirthday('2016-4-8'); console.log(person.getBirthday());
一、精分面向過程的寫法
要知道,上面的面向過程person是一個單例,這種寫法更像是一種命名空間提供工具函數的方式,如——

1 /** 2 * @file cookie 3 * @author 4 */ 5 define(function (require, exports, module) { 6 7 /** 8 * 操作 cookie 9 * 10 * 對外暴露三個方法: 11 * 12 * get() 13 * set() 14 * remove() 15 * 16 * 使用 cookie 必須了解的知識: 17 * 18 * 一枚 cookie 有如下屬性: 19 * 20 * key value domain path expires secure 21 * 22 * domain: 瀏覽器只向指定域的服務器發送 cookie,默認是產生 Set-Cookie 響應的服務器的主機名 23 * path: 為特定頁面指定 cookie,默認是產生 Set-Cookie 響應的 URL 的路徑 24 * expires: 日期格式為(Weekday, DD-MON-YY HH:MM:SS GMT)唯一合法的時區是 GMT,默認是會話結束時過期 25 * secure: 使用 ssl 安全連接時才會發送 cookie 26 * 27 * 有點類似命名空間的意思 28 * 29 */ 30 31 'use strict'; 32 33 /** 34 * 一小時的毫秒數 35 * 36 * @inner 37 * @const 38 * @type {number} 39 */ 40 var HOUR_TIME = 60 * 60 * 1000; 41 42 /** 43 * 把 cookie 字符串解析成對象 44 * 45 * @inner 46 * @param {string} cookieStr 格式為 key1=value1;key2=value2; 47 * @return {Object} 48 */ 49 function parse(cookieStr) { 50 51 if (cookieStr.indexOf('"') === 0) { 52 // 如果 cookie 按照 RFC2068 規范進行了轉義,要轉成原始格式 53 cookieStr = cookieStr.slice(1, -1) 54 .replace(/\\"/g, '"') 55 .replace(/\\\\/g, '\\'); 56 } 57 58 var result = { }; 59 60 try { 61 // Replace server-side written pluses with spaces. 62 // If we can't decode the cookie, ignore it, it's unusable. 63 // If we can't parse the cookie, ignore it, it's unusable. 64 cookieStr = decodeURIComponent(cookieStr.replace(/\+/g, ' ')); 65 66 $.each( 67 cookieStr.split(';'), 68 function (index, part) { 69 var pair = part.split('='); 70 var key = $.trim(pair[0]); 71 var value = $.trim(pair[1]); 72 73 if (key) { 74 result[key] = value; 75 } 76 } 77 ); 78 } 79 catch (e) { } 80 81 return result; 82 } 83 84 /** 85 * 設置一枚 cookie 86 * 87 * @param {string} key 88 * @param {string} value 89 * @param {Object} options 90 */ 91 function setCookie(key, value, options) { 92 93 var expires = options.expires; 94 95 if ($.isNumeric(expires)) { 96 var hours = expires; 97 expires = new Date(); 98 expires.setTime(expires.getTime() + hours * HOUR_TIME); 99 } 100 101 document.cookie = [ 102 encodeURIComponent(key), '=', encodeURIComponent(value), 103 expires ? ';expires=' + expires.toUTCString() : '', 104 options.path ? ';path=' + options.path : '', 105 options.domain ? ';domain=' + options.domain : '', 106 options.secure ? ';secure' : '' 107 ].join(''); 108 } 109 110 /** 111 * 讀取 cookie 的鍵值 112 * 113 * 如果不傳 key,則返回完整的 cookie 鍵值對象 114 * 115 * @param {string=} key 116 * @return {string|Object|undefined} 117 */ 118 exports.get = function (key) { 119 var result = parse(document.cookie); 120 return $.type(key) === 'string' ? result[key] : result; 121 }; 122 123 /** 124 * 寫入 cookie 125 * 126 * @param {string|Object} key 如果 key 是 string,則必須傳 value 127 * 如果 key 是 Object,可批量寫入 128 * @param {*=} value 129 * @param {Object=} options 130 * @property {number=} options.expires 過期小時數,如 1 表示 1 小時后過期 131 * @property {string=} options.path 路徑,默認是 / 132 * @property {string=} options.domain 域名 133 * @property {boolean=} options.secure 是否加密傳輸 134 */ 135 exports.set = function (key, value, options) { 136 137 if ($.isPlainObject(key)) { 138 options = value; 139 value = null; 140 } 141 142 options = $.extend({ }, exports.defaultOptions, options); 143 144 if (value === null) { 145 $.each( 146 key, 147 function (key, value) { 148 setCookie(key, value, options); 149 } 150 ); 151 } 152 else { 153 setCookie(key, value, options); 154 } 155 }; 156 157 /** 158 * 刪除某個 cookie 159 * 160 * @param {string} key 161 * @param {Object=} options 162 * @property {string=} options.path cookie 的路徑 163 * @property {string=} options.domain 域名 164 * @property {boolean=} options.secure 是否加密傳輸 165 */ 166 exports.remove = function (key, options) { 167 168 if (key == null) { 169 return; 170 } 171 172 options = options || { }; 173 options.expires = -1; 174 175 setCookie( 176 key, 177 '', 178 $.extend({ }, exports.defaultOptions, options) 179 ); 180 }; 181 182 /** 183 * 默認屬性,暴露給外部修改 184 * 185 * @type {Object} 186 */ 187 exports.defaultOptions = { 188 path: '/' 189 }; 190 191 });
對於這個person單例或者理解為一個普通的(命名空間)對象,我們會發現兩個工具函數(用於birthday的格式化)——
getTimestampOfInput:服務於setBirthday這個方法
getFormattedDay:服務於getBirthday這個方法
1.1 將工具函數私有性封裝,利用閉包緩存該工具函數
會發現,每一次執行setBirthday,都會創建getTimestampOfInput這個函數,執行完setBirthday之后,getTimestampOfInput又會被銷毀;同理getFormattedDay方法。私有性,我們做到了,但是每一次都需要去創建工具函數(getTimestampOfInput和getFormattedDay)。如果我們想把工具函數僅僅執行一次,可以這樣寫——
// 常用模式一:單例/靜態 - 私有變量&共有方法 // 生成一個人 var person = (function() { // 私有變量 var name = ''; var birthday = new Date().getTime(); // 默認是時間戳方式 // 共有方法 return { setName: function(newName) { name = newName; }, getName: function() { return name; }, setBirthday: (function() { // 私有函數 function getTimestampOfInput(dateString) { return new Date(dateString).getTime(); } return function(dateString) { getTimestampOfInput(dateString); }; })(), getBirthday: (function() { // 函數式 - 不訪問外界變量,沒有閉包的呈現 // 有了輸入,便有了預想中的輸出,不保存狀態 // 私有函數 - 已工具方法存在 function getFormattedDay(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } return function() { return getFormattedDay(birthday); }; })() }; })();
要看見里面用了一層閉包哦,也就是多需要耗損內存,但換來了性能上的優化。
1.2 將工具函數抽取為私有
我們繼續變態的走下去,把這兩個工具函數抽取出來,如——
// 常用模式一:單例/靜態 - 私有變量&共有方法 // 生成一個人 var person = (function() { // 私有變量 var name = ''; var birthday = new Date().getTime(); // 默認是時間戳方式 // 函數式 - 不訪問外界變量,沒有閉包的呈現 // 有了輸入,便有了預想中的輸出,不保存狀態 // 私有函數 - 已工具方法存在 function getFormattedDay(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } // 函數式 - 不訪問外界變量,沒有閉包的呈現 // 有了輸入,便有了預想中的輸出,不保存狀態 // 私有函數 - 已工具方法存在 function getTimestampOfInput(dateString) { return new Date(dateString).getTime(); } // 共有方法 return { setName: function(newName) { name = newName; }, getName: function() { return name; }, setBirthday: function(dateString) { birthday = getTimestampOfInput(dateString); }, getBirthday: function() { return getFormattedDay(birthday); } }; })();
那么這兩個工具方法同樣具有私有性,但是它能夠服務的方法就更多了,所有對外暴露的方法(如將來有個新的方法getCreateDay),都可以使用這兩個工具函數。
1.3 將工具函數顯示聲明為私有
OK,我們看到上面的例子中,name,birthday,包含兩個工具方法都是私有的,我們可以使用"_"的方式來顯示聲明它是私有的,就可以這樣去改裝——
// 常用模式一:靜態私有變量&共有方法 // 生成一個人 var person = { // 單例的私有屬性 - 或者可理解為靜態變量 _name: '', // 單例的私有屬性 - 或者可理解為靜態變量 _birthday: new Date().getTime(), // 默認是時間戳方式 // 工具函數 _getTimestampOfInput: function(dateString) { return new Date(dateString).getTime(); }, // 工具函數 _getFormattedDay: function(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); }, // 共有方法 setName: function(newName) { this._name = newName; }, getName: function() { return this._name; }, setBirthday: function(dateString) { this._birthday = this._getTimestampOfInput(dateString); }, getBirthday: function() { return this._getFormattedDay(this._birthday); } };
看起來還不錯,但是私有屬性還是可以被訪問的,如person._birthday,
1.4 利用private和public命名空間來實現私有和共有
那么,我們想要讓私有的屬性達到真正的私有,並借助命名空間的方式,會有這個方式——
// 常用模式一:靜態私有變量&共有方法 // 生成一個人 var person = (function() { // 該對象保存靜態屬性 // 保存單例的狀態 var _private = { // 單例的私有屬性 - 或者可理解為靜態變量 _name: '', // 單例的私有屬性 - 或者可理解為靜態變量 _birthday: new Date().getTime(), // 默認是時間戳方式 // 工具函數 getTimestampOfInput: function(dateString) { return new Date(dateString).getTime(); }, // 工具函數 _getFormattedDay: function(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); }, getFormattedDayOfBirthday: function() { return this._getFormattedDay(this._birthday); } }; // 共有對象 var _public = { setName: function(newName) { _private._name = newName; }, // 直接從_private對象中獲取 getName: function() { return _private._name; }, /** * 可直接操作_private中的靜態屬性 * @param dateString */ setBirthday: function(dateString) { _private._birthday = _private.getTimestampOfInput(dateString); }, getBirthday: function() { return _private.getFormattedDayOfBirthday(); } }; return _public; })();
_private和_public這兩個命名空間還不錯。在此基礎上,建議把工具函數拿出來,可以這樣——
// 常用模式一:靜態私有變量&共有方法 // 生成一個人 var person = (function() { // 工具函數 // 可供_private和_public對象共用 function getTimestampOfInput(dateString) { return new Date(dateString).getTime(); } // 工具函數 // 可供_private和_public對象共用 function getFormattedDay(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } // 該對象保存靜態屬性 // 保存單例的狀態 var _private = { // 單例的私有屬性 - 或者可理解為靜態變量 _name: '', // 單例的私有屬性 - 或者可理解為靜態變量 _birthday: new Date().getTime() // 默認是時間戳方式 }; // 共有對象 var _public = { setName: function(newName) { _private._name = newName; }, // 直接從_private對象中獲取 getName: function() { return _private._name; }, /** * 可直接操作_private中的靜態屬性 * @param dateString */ setBirthday: function(dateString) { _private._birthday = getTimestampOfInput(dateString); }, getBirthday: function() { return getFormattedDay(_private._birthday); } }; return _public; })();
1.5 將工具函數就近於它的調用者
有些同學非常喜歡將工具函數靠近與它的調用者,類似於這樣——
// 常用模式一:靜態私有變量&共有方法 // 生成一個人 var person = (function() { // 該對象保存靜態屬性 // 保存單例的狀態 var _private = { // 單例的私有屬性 - 或者可理解為靜態變量 _name: '', // 單例的私有屬性 - 或者可理解為靜態變量 _birthday: new Date().getTime() // 默認是時間戳方式 }; _private.name = ''; _private.birthday = new Date().getTime(); // 默認是時間戳方式 var _public = {}; _public.setName = function(newName) { _private._name = newName; }; _public.getName = function() { return _private._name; }; // 工具函數 // 可供_private和_public對象共用 function getTimestampOfInput(dateString) { return new Date(dateString).getTime(); } _public.setBirthday = function(dateString) { _private._birthday = getTimestampOfInput(dateString); }; // 工具函數 // 可供_private和_public對象共用 function getFormattedDay(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } _public.getBirthday = function() { return getFormattedDay(_private._birthday); }; return _public; })();
1.6 將工具函數放入util等全局命名空間
同樣的,我們發現這兩個工具函數具有通用性,可以放置於全局,供所有函數使用,那么就有這樣的方式,如——
// 這里的工具類,可以以單獨文件存在,供全局工程來使用 var util = { /** * 生日格式化顯示 * @param timestamp * @returns {string} * @private */ getFormattedDay: function(timestamp) { // 模擬實現靜態方法 var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); }, /** * 根據用戶輸入來獲取時間戳,如輸入'1995-10-05' * @param timestamp * @returns {string} * @private */ getTimestampOfInput: function(dateString) { return new Date(dateString).getTime(); } }; var person = (function() { // 私有變量 var name = ''; var birthday = new Date().getTime(); // 默認是時間戳方式 // 共有方法 return { setName: function(newName) { name = newName; }, getName: function() { return name; }, setBirthday: function(dateString) { birthday = util.getTimestampOfInput(dateString); }, getBirthday: function() { return util.getFormattedDay(birthday); } }; })();
上面這種方式,也是我們最常用的方式,很直觀,易維護。
OK,那么面向過程的寫法方式,就算是精分完了,很變態對不對?
總之,沒有嚴格的對錯,按照你認同喜歡的模式來。下面精分一下面向對象的寫法。
二、精分面向對象的寫法
面向對象的寫法,要注意prototype中的方法供所有實例對象所共有,且這里的方法都是對實例狀態變更的說明,即對實例屬性的操作的變更。
2.1 不要把工具函數放入prototype中
基於前言里面的例子,我們常常不注意的將工具函數也都放在prototype當中,如——
// 多實例 var Person = function(name, birthday) { this.name = name; this.birthday = birthday; // timestamp }; Person.prototype = { setName: function(name) { this.name = name; }, getName: function() { return this.name; }, /** * 設置生日 * @param dateString */ setBirthday: function(dateString) { this.birthday = this._getTimestampOfInput(dateString); }, // 工具函數 _getTimestampOfInput: function(dateString) { return new Date(dateString).getTime(); }, /** * 獲取生日 * @returns {*} */ getBirthday: function() { return this._getFormattedDay(this.birthday); }, // 工具函數 _getFormattedDay: function(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } };
會看見上面的_getTimestampOfInput和_getFormattedDay兩個方法也都放置在了prototype當中,然而這里的方法並沒有操作實例屬性,因此不應該將這類工具方法置於prototype當中。
2.2 不要將緩存變量放入this當中
還有一個大家常常犯的一個大錯誤,就是習慣性把各個方法間通訊的變量放入到this當中,如下——
var Person = function(name, birthday) { this.name = name; this.birthday = birthday; // timestamp }; function getTimestampOfInput(dateString) { return new Date(dateString).getTime(); } function getFormattedDay(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } Person.prototype = { setName: function(name) { this.name = name; }, getName: function() { return this.name; }, /** * 設置生日 * @param dateString */ setBirthday: function(dateString) { this.birthday = getTimestampOfInput(dateString); }, /** * 獲取生日 * @returns {*} */ getBirthday: function() { // 不要把緩存變量放置於this中 this.birthdayOfFormatted = getFormattedDay(this.birthday); return this.birthdayOfFormatted; } };
會看到,這里的this.birthdayOfFormatted是一個緩存變量,並不能代表這個實例的某個狀態。好了,我們回到正確的方式。
2.3 將工具函數就近於方法的調用者
// 多實例 - 抽取工具函數 var Person = function(name, birthday) { this.name = name; this.birthday = birthday; // timestamp }; Person.prototype.setName = function(name) { this.name = name; }; Person.prototype.getName = function() { return this.name; }; // 工具函數 function getTimestampOfInput(dateString) { return new Date(dateString).getTime(); } Person.prototype.setBirthday = function(dateString) { this.birthday = getTimestampOfInput(dateString); }; // 工具函數 function getFormattedDay(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); } Person.prototype.getBirthday = function() { return getFormattedDay(this.birthday); };
在維護性方面略勝一籌,主要看個人的變成習慣。
2.4 將工具函數放入類命名空間中,充當類的靜態函數
// 多實例 - 抽取工具函數 var Person = function(name, birthday) { this.name = name; this.birthday = birthday; // timestamp }; // 工具函數 - 對外靜態變量 Person.getTimestampOfInput = function (dateString) { return new Date(dateString).getTime(); }; // 工具函數 - 對外靜態變量 Person.getFormattedDay = function(timestamp) { var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); }; Person.prototype = { setName: function(name) { this.name = name; }, getName: function() { return this.name; }, /** * 設置生日 * @param dateString */ setBirthday: function(dateString) { this.birthday = Person.getTimestampOfInput(dateString); }, /** * 獲取生日 * @returns {*} */ getBirthday: function() { return Person.getFormattedDay(this.birthday); } };
我個人比較推薦這種寫法,當然也可以把工具函數放入某個類似於util的命名空間中,供全局調用。
2.5 將工具函數放入util等全局命名空間
// 這里的工具類,可以以單獨文件存在,供全局工程來使用 var util = { /** * 生日格式化顯示 * @param timestamp * @returns {string} * @private */ getFormattedDay: function(timestamp) { // 模擬實現靜態方法 var datetime = new Date(timestamp); var year = datetime.getFullYear(); var month = datetime.getMonth() + 1; var date = datetime.getDate(); return year + '-' + (String(month).length < 2 ? "0" + month : month) + "-" + (String(date).length < 2 ? "0" + date : date); }, /** * 根據用戶輸入來獲取時間戳,如輸入'1995-10-05' * @param timestamp * @returns {string} * @private */ getTimestampOfInput: function(dateString) { return new Date(dateString).getTime(); } }; // 多實例 - 抽取工具函數 var Person = function(name, birthday) { this.name = name; this.birthday = birthday; // timestamp }; Person.prototype = { setName: function(name) { this.name = name; }, getName: function() { return this.name; }, /** * 設置生日 * @param dateString */ setBirthday: function(dateString) { this.birthday = util.getTimestampOfInput(dateString); }, /** * 獲取生日 * @returns {*} */ getBirthday: function() { return util.getFormattedDay(this.birthday); } };
好啦,整個面向對象的寫法方式介紹到這兒。
總之,歸於一點——要知道什么方法可以當做工具函數處理,並合理地放置工具函數的位置。
三、總結
整篇文章主要圍繞工具函數的寫法展開,模式不同,沒有對與錯,依照自身的編碼習慣而定。歡迎看到文章的博友補充。