谈JavaScript代码封装

时间:2023-03-09 14:48:43
谈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是一个单例,这种写法更像是一种命名空间提供工具函数的方式,如——

 /**
* @file cookie
* @author
*/
define(function (require, exports, module) { /**
* 操作 cookie
*
* 对外暴露三个方法:
*
* get()
* set()
* remove()
*
* 使用 cookie 必须了解的知识:
*
* 一枚 cookie 有如下属性:
*
* key value domain path expires secure
*
* domain: 浏览器只向指定域的服务器发送 cookie,默认是产生 Set-Cookie 响应的服务器的主机名
* path: 为特定页面指定 cookie,默认是产生 Set-Cookie 响应的 URL 的路径
* expires: 日期格式为(Weekday, DD-MON-YY HH:MM:SS GMT)唯一合法的时区是 GMT,默认是会话结束时过期
* secure: 使用 ssl 安全连接时才会发送 cookie
*
* 有点类似命名空间的意思
*
*/ 'use strict'; /**
* 一小时的毫秒数
*
* @inner
* @const
* @type {number}
*/
var HOUR_TIME = 60 * 60 * 1000; /**
* 把 cookie 字符串解析成对象
*
* @inner
* @param {string} cookieStr 格式为 key1=value1;key2=value2;
* @return {Object}
*/
function parse(cookieStr) { if (cookieStr.indexOf('"') === 0) {
// 如果 cookie 按照 RFC2068 规范进行了转义,要转成原始格式
cookieStr = cookieStr.slice(1, -1)
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\');
} var result = { }; try {
// Replace server-side written pluses with spaces.
// If we can't decode the cookie, ignore it, it's unusable.
// If we can't parse the cookie, ignore it, it's unusable.
cookieStr = decodeURIComponent(cookieStr.replace(/\+/g, ' ')); $.each(
cookieStr.split(';'),
function (index, part) {
var pair = part.split('=');
var key = $.trim(pair[0]);
var value = $.trim(pair[1]); if (key) {
result[key] = value;
}
}
);
}
catch (e) { } return result;
} /**
* 设置一枚 cookie
*
* @param {string} key
* @param {string} value
* @param {Object} options
*/
function setCookie(key, value, options) { var expires = options.expires; if ($.isNumeric(expires)) {
var hours = expires;
expires = new Date();
expires.setTime(expires.getTime() + hours * HOUR_TIME);
} document.cookie = [
encodeURIComponent(key), '=', encodeURIComponent(value),
expires ? ';expires=' + expires.toUTCString() : '',
options.path ? ';path=' + options.path : '',
options.domain ? ';domain=' + options.domain : '',
options.secure ? ';secure' : ''
].join('');
} /**
* 读取 cookie 的键值
*
* 如果不传 key,则返回完整的 cookie 键值对象
*
* @param {string=} key
* @return {string|Object|undefined}
*/
exports.get = function (key) {
var result = parse(document.cookie);
return $.type(key) === 'string' ? result[key] : result;
}; /**
* 写入 cookie
*
* @param {string|Object} key 如果 key 是 string,则必须传 value
* 如果 key 是 Object,可批量写入
* @param {*=} value
* @param {Object=} options
* @property {number=} options.expires 过期小时数,如 1 表示 1 小时后过期
* @property {string=} options.path 路径,默认是 /
* @property {string=} options.domain 域名
* @property {boolean=} options.secure 是否加密传输
*/
exports.set = function (key, value, options) { if ($.isPlainObject(key)) {
options = value;
value = null;
} options = $.extend({ }, exports.defaultOptions, options); if (value === null) {
$.each(
key,
function (key, value) {
setCookie(key, value, options);
}
);
}
else {
setCookie(key, value, options);
}
}; /**
* 删除某个 cookie
*
* @param {string} key
* @param {Object=} options
* @property {string=} options.path cookie 的路径
* @property {string=} options.domain 域名
* @property {boolean=} options.secure 是否加密传输
*/
exports.remove = function (key, options) { if (key == null) {
return;
} options = options || { };
options.expires = -1; setCookie(
key,
'',
$.extend({ }, exports.defaultOptions, options)
);
}; /**
* 默认属性,暴露给外部修改
*
* @type {Object}
*/
exports.defaultOptions = {
path: '/'
}; });

对于这个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);
}
};

好啦,整个面向对象的写法方式介绍到这儿。

总之,归于一点——要知道什么方法可以当做工具函数处理,并合理地放置工具函数的位置。

三、总结

整篇文章主要围绕工具函数的写法展开,模式不同,没有对与错,依照自身的编码习惯而定。欢迎看到文章的博友补充。