谈JavaScript代码封装
前言
也算老生常谈的问题了,再深入搞一搞怎么玩儿封装,如果看到这篇文章的你,正好你也是追求完美的代码洁癖狂者,那么这篇文章相信非常适合你。
举一个例子,编写一个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); } };
好啦,整个面向对象的写法方式介绍到这儿。
总之,归于一点——要知道什么方法可以当做工具函数处理,并合理地放置工具函数的位置。
三、总结
整篇文章主要围绕工具函数的写法展开,模式不同,没有对与错,依照自身的编码习惯而定。欢迎看到文章的博友补充。