面向对象的Javascript之三(封装和信息隐藏)


Posted in Javascript onJanuary 27, 2012

同时,我们知道在面向对象的高级语言中,创建包含私有成员的对象是最基本的特性之一,提供属性和方法对私有成员进行访问来隐藏内部的细节。虽然JS也是面向对象的,但没有内部机制可以直接表明一个成员是公有还是私有的。还是那句话,依靠JS的语言灵活性,我们可以创建公共、私有和特权成员,信息隐藏是我们要实现的目标,而封装是我们实现这个目标的方法。我们还是从一个示例来说明:创建一个类来存储图书数据,并实现可以在网页中显示这些数据。

1. 最简单的是完全暴露对象。使用构造函数创建一个类,其中所有的属性和方法在外部都是可以访问的。

var Book = function(isbn, title, author) { 
if(isbn == undefined) { 
throw new Error("Book constructor requires a isbn."); 
} 
this.isbn = isbn; 
this.title = title || ""; 
this.author = author || ""; 
} 
Book.prototype.display = function() { 
return "Book: ISBN: " + this.isbn + ",Title: " + this.title + ",Author: " + this.author; 
}

display方法依赖于isbn是否正确,如果不是你将无法获取图像以及链接。考虑到这点,每本图书isbn必须存在的,而图书的标题和作者是可选的。表面上看只要指定一个isbn参数似乎就能正常运行。但却不能保证isbn的完整性,基于此我们加入isbn的验证,使图书的检查更加健壮。
var Book = function(isbn, title, author) { 
if(!this.checkIsbn(isbn)) { 
throw new Error("Book: invalid ISBN."); 
} 
this.isbn = isbn; 
this.title = title || ""; 
this.author = author || ""; 
} 
Book.prototype = { 
checkIsbn: function(isbn) { 
if(isbn == undefined || typeof isbn != "string") return false; 
isbn = isbn.replace("-", ""); 
if(isbn.length != 10 && isbn.length != 13) return false; 
var sum = 0; 
if(isbn.length == 10) { 
if(!isbn.match(\^\d{9}\)) return false; 
for(var i = 0;i < 9;i++) { 
sum += isbn.charAt(i) * (10 - i); 
} 
var checksum = sum % 11; 
if(checksum == 10) checksum = "X"; 
if(isbn.charAt(9) != checksum) return false; 
} else { 
if(!isbn.match(\^\d{12}\)) return false; 
for(var i = 0;i < 12;i++) { 
sum += isbn.charAt(i) * (i % 2 == 0 ? 1 : 3); 
} 
var checksum = sum % 10; 
if(isbn.charAt(12) != checksum) return false; 
} 
return true; 
}, 
display: function() { 
return "Book: ISBN: " + this.isbn + ",Title: " + this.title + ",Author: " + this.author; 
} 
};

我们添加了checkIsbn()来验证ISBN的有效性,确保display()可以正常运行。但是需求有变化了,每本书可能有多个版本,意味着同一本可能有多个ISBN号存在,需要维护单独的选择版本的算法来控制。同时尽管能检查数据的完整性,但却无法控制外部对内部成员的访问(如对isbn,title,author赋值),就谈不上保护内部数据了。我们继续改进这个方案,采用接口实现(提供get访问器/set存储器)。
var Publication = new Interface("Publication", ["getIsbn", "setIsbn", "checkIsbn", "getTitle", "setTitle", "getAuthor", "setAuthor", "display"]); 
var Book = function(isbn, title, author) { 
// implements Publication interface 
this.setIsbn(isbn); 
this.setTitle(title); 
this.setAuthor(author); 
} 
Book.prototype = { 
getIsbn: function() { 
return this.isbn; 
}, 
setIsbn: function(isbn) { 
if(!this.checkIsbn(isbn)) { 
throw new Error("Book: Invalid ISBN."); 
} 
this.isbn = isbn; 
}, 
checkIsbn: function(isbn) { 
if(isbn == undefined || typeof isbn != "string") return false; 
isbn = isbn.replace("-", ""); 
if(isbn.length != 10 && isbn.length != 13) return false; 
var sum = 0; 
if(isbn.length == 10) { 
if(!isbn.match(\^\d{9}\)) return false; 
for(var i = 0;i < 9;i++) { 
sum += isbn.charAt(i) * (10 - i); 
} 
var checksum = sum % 11; 
if(checksum == 10) checksum = "X"; 
if(isbn.charAt(9) != checksum) return false; 
} else { 
if(!isbn.match(\^\d{12}\)) return false; 
for(var i = 0;i < 12;i++) { 
sum += isbn.charAt(i) * (i % 2 == 0 ? 1 : 3); 
} 
var checksum = sum % 10; 
if(isbn.charAt(12) != checksum) return false; 
} 
return true; 
}, 
getTitle: function() { 
return this.title; 
}, 
setTitle: function(title) { 
this.title = title || ""; 
}, 
getAuthor: function() { 
return this.author; 
}, 
setAuthor: function(author) { 
this.author = author || ""; 
}, 
display: function() { 
return "Book: ISBN: " + this.isbn + ",Title: " + this.title + ",Author: " + this.author; 
} 
};

现在就可以通过接口Publication来与外界进行通信。赋值方法也在构造器内部完成,不需要实现两次同样的验证,看似非常完美的完全暴露对象方案了。虽然能通过set存储器来设置属性,但这些属性仍然是公有的,可以直接赋值。但此方案到此已经无能为力了,我会在第二种信息隐藏解决方案中来优化。尽管如此,此方案对于那些没有深刻理解作用域的新手非常容易上手。唯一的不足是不能保护内部数据且存储器增加了多余的不必要代码。
2. 使用命名规则的私有方法。就是使用下划线来标识私有成员,避免无意中对私有成员进行赋值,本质上与完全暴露对象是一样的。但这却避免了第一种方案无意对私有成员进行赋值操作,却依然不能避免有意对私有成员进行设置。只是说定义了一种命名规范,需要团队成员来遵守,不算是一种真正的内部信息隐藏的完美方案。
var Publication = new Interface("Publication", ["getIsbn", "setIsbn", "getTitle", "setTitle", "getAuthor", "setAuthor", "display"]); 
var Book = function(isbn, title, author) { 
// implements Publication interface 
this.setIsbn(isbn); 
this.setTitle(title); 
this.setAuthor(author); 
} 
Book.prototype = { 
getIsbn: function() { 
return this._isbn; 
}, 
setIsbn: function(isbn) { 
if(!this._checkIsbn(isbn)) { 
throw new Error("Book: Invalid ISBN."); 
} 
this._isbn = isbn; 
}, 
_checkIsbn: function(isbn) { 
if(isbn == undefined || typeof isbn != "string") return false; 
isbn = isbn.replace("-", ""); 
if(isbn.length != 10 && isbn.length != 13) return false; 
var sum = 0; 
if(isbn.length == 10) { 
if(!isbn.match(\^\d{9}\)) return false; 
for(var i = 0;i < 9;i++) { 
sum += isbn.charAt(i) * (10 - i); 
} 
var checksum = sum % 11; 
if(checksum == 10) checksum = "X"; 
if(isbn.charAt(9) != checksum) return false; 
} else { 
if(!isbn.match(\^\d{12}\)) return false; 
for(var i = 0;i < 12;i++) { 
sum += isbn.charAt(i) * (i % 2 == 0 ? 1 : 3); 
} 
var checksum = sum % 10; 
if(isbn.charAt(12) != checksum) return false; 
} 
return true; 
}, 
getTitle: function() { 
return this._title; 
}, 
setTitle: function(title) { 
this._title = title || ""; 
}, 
getAuthor: function() { 
return this._author; 
}, 
setAuthor: function(author) { 
this._author = author || ""; 
}, 
display: function() { 
return "Book: ISBN: " + this.getIsbn() + ",Title: " + this.getTitle() + ",Author: " + this.getAuthor(); 
} 
};

注意:除了isbn,title,author属性被加上"_"标识为私有成员外,checkIsbn()也被标识为私有方法。

3. 通过闭包来真正私有化成员。如果对闭包概念中的作用域和嵌套函数不熟悉的朋友,可以参考"面向对象的Javascript之一(初识Javascript)"文章,这里不再详细论述。

var Publication = new Interface("Publication", ["getIsbn", "setIsbn", "getTitle", "setTitle", "getAuthor", "setAuthor", "display"]); 
var Book = function(newIsbn, newTitle, newAuthor) { 
// private attribute 
var isbn, title, author; 
// private method 
function checkIsbn(isbn) { 
if(isbn == undefined || typeof isbn != "string") return false; 
isbn = isbn.replace("-", ""); 
if(isbn.length != 10 && isbn.length != 13) return false; 
var sum = 0; 
if(isbn.length == 10) { 
if(!isbn.match(\^\d{9}\)) return false; 
for(var i = 0;i < 9;i++) { 
sum += isbn.charAt(i) * (10 - i); 
} 
var checksum = sum % 11; 
if(checksum == 10) checksum = "X"; 
if(isbn.charAt(9) != checksum) return false; 
} else { 
if(!isbn.match(\^\d{12}\)) return false; 
for(var i = 0;i < 12;i++) { 
sum += isbn.charAt(i) * (i % 2 == 0 ? 1 : 3); 
} 
var checksum = sum % 10; 
if(isbn.charAt(12) != checksum) return false; 
} 
return true; 
} 
// previleged method 
this.getIsbn = function() { 
return isbn; 
}; 
this.setIsbn = function(newIsbn) { 
if(!checkIsbn(newIsbn)) { 
throw new Error("Book: Invalid ISBN."); 
} 
isbn = newIsbn; 
} 
this.getTitle = function() { 
return title; 
}, 
this.setTitle = function(newTitle) { 
title = newTitle || ""; 
}, 
this.getAuthor: function() { 
return author; 
}, 
this.setAuthor: function(newAuthor) { 
author = newAuthor || ""; 
} 
// implements Publication interface 
this.setIsbn(newIsbn); 
this.setTitle(newTitle); 
this.setAuthor(newAuthor); 
} 
// public methods 
Book.prototype = { 
display: function() { 
return "Book: ISBN: " + this.getIsbn() + ",Title: " + this.getTitle() + ",Author: " + this.getAuthor(); 
} 
};

这种方案与上一种有哪些不同呢?首先,在构造器中使用var来声明三个私有成员,同样也声明了私有方法checkIsbn(),仅仅在构造器中有效。使用this关键字声明特权方法,即声明在构造器内部但却可以访问私有成员。任何不需要访问私有成员的方法都在Book.prototype中声明(如:display),也即是将需要访问私有成员的方法声明为特权方法是解决这个问题的关键。但此访问也有一定缺陷,如对每一个实例而言,都要创建一份特权方法的副本,势必需要更多内存。我们继续优化,采用静态成员来解决所面临的问题。顺便提一句:静态成员仅仅属于类,所有的对象仅共用一份副本(在"面向对象的Javascript之二(实现接口)中有说明,参见Interface.ensureImplements方法"),而实例方法是针对对象而言。
var Publication = new Interface("Publication", ["getIsbn", "setIsbn", "getTitle", "setTitle", "getAuthor", "setAuthor", "display"]); 
var Book = (function() { 
// private static attribute 
var numsOfBooks = 0; 
// private static method 
function checkIsbn(isbn) { 
if(isbn == undefined || typeof isbn != "string") return false; 
isbn = isbn.replace("-", ""); 
if(isbn.length != 10 && isbn.length != 13) return false; 
var sum = 0; 
if(isbn.length == 10) { 
if(!isbn.match(\^\d{9}\)) return false; 
for(var i = 0;i < 9;i++) { 
sum += isbn.charAt(i) * (10 - i); 
} 
var checksum = sum % 11; 
if(checksum == 10) checksum = "X"; 
if(isbn.charAt(9) != checksum) return false; 
} else { 
if(!isbn.match(\^\d{12}\)) return false; 
for(var i = 0;i < 12;i++) { 
sum += isbn.charAt(i) * (i % 2 == 0 ? 1 : 3); 
} 
var checksum = sum % 10; 
if(isbn.charAt(12) != checksum) return false; 
} 
return true; 
} 
// return constructor 
return function(newIsbn, newTitle, newAuthor) { 
// private attribute 
var isbn, title, author; 
// previleged method 
this.getIsbn = function() { 
return isbn; 
}; 
this.setIsbn = function(newIsbn) { 
if(!Book.checkIsbn(newIsbn)) { 
throw new Error("Book: Invalid ISBN."); 
} 
isbn = newIsbn; 
} 
this.getTitle = function() { 
return title; 
}, 
this.setTitle = function(newTitle) { 
title = newTitle || ""; 
}, 
this.getAuthor = function() { 
return author; 
}, 
this.setAuthor = function(newAuthor) { 
author = newAuthor || ""; 
} 
Book.numsOfBooks++; 
if(Book.numsOfBooks > 50) { 
throw new Error("Book: at most 50 instances of Book can be created."); 
} 
// implements Publication interface 
this.setIsbn(newIsbn); 
this.setTitle(newTitle); 
this.setAuthor(newAuthor); 
}; 
})(); 
// public static methods 
Book.convertToTitle = function(title) { 
return title.toUpperCase(); 
} 
// public methods 
Book.prototype = { 
display: function() { 
return "Book: ISBN: " + this.getIsbn() + ",Title: " + this.getTitle() + ",Author: " + this.getAuthor(); 
} 
};

这种方案与上种相似,使用var和this来创建私有成员和特权方法。不同之处在于使用闭包来返回构造器,并将checkIsbn声明为私有静态方法。可能有人会问,我为什么要创建私有静态方法,答案在于使所有对象公用一份函数副本而已。我们这里创建的50个实例都只有一个方法副本checkIsbn,且属于类Book。根据需要,你也可以创建公有的静态方法供外部调用(如:convertToTitle)。这里我们继续考虑一个问题,假设以后我们需要对不同的书做限制,比如<<Javascript高级编程>>最大印发量为500,<<.NET>>最大印发量为1000,也即说需要一个最大印发量的常量。思考一下,利用已有的知识,我们如何声明一个常量呢?其实不难,我们想想,可以利用一个只有访问器的私有特权方法就可以实现。
var Publication = new Interface("Publication", ["getIsbn", "setIsbn", "getTitle", "setTitle", "getAuthor", "setAuthor", "display"]); 
var Book = (function() { 
// private static attribute 
var numsOfBooks = 0; 
// private static contant 
var Constants = { 
"MAX_JAVASCRIPT_NUMS": 500, 
"MAX_NET_NUMS": 1000 
}; 
// private static previleged method 
this.getMaxNums(name) { 
return Constants[name.ToUpperCase()]; 
} 
// private static method 
function checkIsbn(isbn) { 
if(isbn == undefined || typeof isbn != "string") return false; 
isbn = isbn.replace("-", ""); 
if(isbn.length != 10 && isbn.length != 13) return false; 
var sum = 0; 
if(isbn.length == 10) { 
if(!isbn.match(\^\d{9}\)) return false; 
for(var i = 0;i < 9;i++) { 
sum += isbn.charAt(i) * (10 - i); 
} 
var checksum = sum % 11; 
if(checksum == 10) checksum = "X"; 
if(isbn.charAt(9) != checksum) return false; 
} else { 
if(!isbn.match(\^\d{12}\)) return false; 
for(var i = 0;i < 12;i++) { 
sum += isbn.charAt(i) * (i % 2 == 0 ? 1 : 3); 
} 
var checksum = sum % 10; 
if(isbn.charAt(12) != checksum) return false; 
} 
return true; 
} 
// return constructor 
return function(newIsbn, newTitle, newAuthor) { 
// private attribute 
var isbn, title, author; 
// previleged method 
this.getIsbn = function() { 
return isbn; 
}; 
this.setIsbn = function(newIsbn) { 
if(!Book.checkIsbn(newIsbn)) { 
throw new Error("Book: Invalid ISBN."); 
} 
isbn = newIsbn; 
} 
this.getTitle = function() { 
return title; 
}, 
this.setTitle = function(newTitle) { 
title = newTitle || ""; 
}, 
this.getAuthor = function() { 
return author; 
}, 
this.setAuthor = function(newAuthor) { 
author = newAuthor || ""; 
} 
Book.numsOfBooks++; 
if(Book.numsOfBooks > 50) { 
throw new Error("Book: at most 50 instances of Book can be created."); 
} 
// implements Publication interface 
this.setIsbn(newIsbn); 
this.setTitle(newTitle); 
this.setAuthor(newAuthor); 
}; 
})(); 
// public static methods 
Book.convertToTitle = function(title) { 
return title.toUpperCase(); 
} 
// public methods 
Book.prototype = { 
display: function() { 
return "Book: ISBN: " + this.getIsbn() + ",Title: " + this.getTitle() + 
",Author: " + this.getAuthor() + ", Maximum: "; 
}, 
showMaxNums: function() { 
return Book.getMaxNums("MAX_JAVASCRIPT_NUMS"); 
} 
};

最完美的情况就是你所封装的程序对调用者而言,仅仅需要知道你的接口就可以,根本不关心你如何实现。但问题在于,随着工程量的扩大,你的封装内容必然会增大,在项目发生交接时,对于一个对作用域和闭包等概念不熟悉的成员来说,维护难度会变得如此之大。有些时候应需求响应必须改动源码(这里不一定指改接口),可能是新增一些细节,即使拿到你的源码却无从下手,那就不好做了。因此,我的建议:封装不要过度,接口一定要清晰,可扩展。
Javascript 相关文章推荐
论坛特效代码收集(落伍转发-不错)
Dec 02 Javascript
自动完成JS类(纯JS, Ajax模式)
Mar 12 Javascript
JQUERY简单按钮轮换选中效果实现方法
May 07 Javascript
基于jQuery实现在线选座之高铁版
Aug 24 Javascript
jqGrid表格应用之新增与删除数据附源码下载
Dec 02 Javascript
vuejs使用axios异步访问时用get和post的实例讲解
Aug 09 Javascript
Vue渲染过程浅析
Mar 14 Javascript
了解javascript中变量及函数的提升
May 27 Javascript
javascript刷新父页面方法汇总详解
Oct 10 Javascript
vue页面更新patch的实现示例
Mar 25 Javascript
基于JavaScript的数据结构队列动画实现示例解析
Aug 06 Javascript
教你使用vscode 搭建react-native开发环境
Jul 07 Javascript
面向对象的Javascript之二(接口实现介绍)
Jan 27 #Javascript
js String对象中常用方法小结(字符串操作)
Jan 27 #Javascript
getElementByIdx_x js自定义getElementById函数
Jan 24 #Javascript
基于JQUERY的多级联动代码
Jan 24 #Javascript
JavaScript常用对象的方法和属性小结
Jan 24 #Javascript
DOM2非标准但却支持很好的几个属性小结
Jan 21 #Javascript
用js来定义浏览器中一个左右浮动元素相对于页面主体宽度的位置的函数
Jan 21 #Javascript
You might like
php中判断一个字符串包含另一个字符串的方法
2007/03/19 PHP
PHP 时间转换Unix时间戳代码
2010/01/22 PHP
PHP实现简单搜歌的方法
2015/07/28 PHP
PHP下 Mongodb 连接远程数据库的实例代码
2017/08/30 PHP
PHP中类与对象功能、用法实例解读
2020/03/27 PHP
查看大图功能代码jquery版
2013/11/05 Javascript
使用Sticker.js实现贴纸效果
2015/01/28 Javascript
Javascript中的call()方法介绍
2015/03/15 Javascript
jquery ajax结合thinkphp的getjson实现跨域的方法
2016/06/06 Javascript
基于JavaScript实现的折半查找算法示例
2017/04/14 Javascript
jquery.validate.js 多个相同name的处理方式
2017/07/10 jQuery
限时抢购-倒计时的完整实例(分享)
2017/09/17 Javascript
node文件批量重命名的方法示例
2017/10/23 Javascript
微信小程序实现动态设置placeholder提示文字及按钮选中/取消状态的方法
2017/12/14 Javascript
Vue header组件开发详解
2018/01/26 Javascript
express如何使用session与cookie的方法
2018/01/30 Javascript
vue移动端实现红包雨效果
2020/06/23 Javascript
vue中将html字符串转换成html后遇到的问题小结
2018/12/10 Javascript
JavaScript键盘事件常见用法实例分析
2019/01/03 Javascript
微信小程序 云开发模糊查询实现解析
2019/09/02 Javascript
用Python实现一个简单的能够发送带附件的邮件程序的教程
2015/04/08 Python
Python中使用items()方法返回字典元素对的教程
2015/05/21 Python
Python中enumerate()函数编写更Pythonic的循环
2018/03/06 Python
Python使用matplotlib绘图无法显示中文问题的解决方法
2018/03/14 Python
python3中函数参数的四种简单用法
2018/07/09 Python
django form和field具体方法和属性说明
2020/07/09 Python
车间操作工岗位职责
2013/12/19 职场文书
机械设计及其自动化专业求职信
2014/06/09 职场文书
节约每一滴水演讲稿
2014/09/09 职场文书
国际政治学专业推荐信
2014/09/26 职场文书
中学生逃课检讨书
2015/02/17 职场文书
合作协议书格式范本
2016/03/21 职场文书
导游词之塘栖古镇
2019/12/04 职场文书
Ajax常用封装库——Axios的使用
2021/05/08 Javascript
mysql如何配置白名单访问
2021/06/30 MySQL
spring IOC容器的Bean管理XML自动装配过程
2022/05/30 Java/Android