面向对象的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 相关文章推荐
拖动一个HTML元素
Dec 22 Javascript
JavaScript初学者应注意的七个细节详细介绍
Dec 27 Javascript
浅谈JavaScript的事件
Feb 27 Javascript
js实现无缝滚动特效
Dec 20 Javascript
bootstrap的3级菜单样式,支持母版页保留打开状态实现方法
Nov 10 Javascript
原生js实现旋转木马轮播图效果
Feb 27 Javascript
Angularjs过滤器实现动态搜索与排序功能示例
Dec 13 Javascript
微信公众平台获取access_token的方法步骤
Mar 29 Javascript
解决JQuery的ajax函数执行失败alert函数弹框一闪而过问题
Apr 10 jQuery
Vue+Koa2 打包后进行线上部署的教程详解
Jul 31 Javascript
koa-passport实现本地验证的方法示例
Feb 20 Javascript
react antd表格中渲染一张或多张图片的实例
Oct 28 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 团购折扣计算公式
2011/11/24 PHP
php 目录遍历、删除 函数的使用介绍
2013/04/28 PHP
浅谈PHP中output_buffering
2015/07/13 PHP
PDO操作MySQL的基础教程(推荐)
2017/08/18 PHP
用Javascript数组处理多个字符串的连接问题
2009/08/20 Javascript
JQuery select标签操作代码段
2010/05/16 Javascript
jQuery基础知识filter()和find()实例说明
2010/07/06 Javascript
js前台判断开始时间是否小于结束时间
2012/02/23 Javascript
BootStrap栅格系统、表单样式与按钮样式源码解析
2017/01/20 Javascript
canvas绘制一个常用的emoji表情
2017/03/30 Javascript
巧用weui.topTips验证数据的实例
2017/04/17 Javascript
微信小程序教程系列之新建页面(4)
2017/04/17 Javascript
jQuery Tree Multiselect使用详解
2017/05/02 jQuery
JavaScript检查数据中是否存在相同的元素(两种方法)
2018/10/07 Javascript
详解单页面路由工程使用微信分享及二次分享解决方案
2019/02/22 Javascript
vue.js实现简单的计算器功能
2020/02/22 Javascript
vue中的双向数据绑定原理与常见操作技巧详解
2020/03/16 Javascript
jQuery实现容器间的元素拖拽功能
2020/12/01 jQuery
Python首次安装后运行报错(0xc000007b)的解决方法
2016/10/18 Python
Python运算符重载详解及实例代码
2017/03/07 Python
win10 64bit下python NLTK安装教程
2018/09/19 Python
python3+PyQt5 数据库编程--增删改实例
2019/06/17 Python
python批量将excel内容进行翻译写入功能
2019/10/10 Python
Python用SSH连接到网络设备
2021/02/18 Python
英国手机零售商:Metrofone
2019/03/18 全球购物
个人自我鉴定范文
2013/10/04 职场文书
分公司经理岗位职责
2013/11/11 职场文书
小学数学国培感言
2014/03/10 职场文书
优秀毕业生就业推荐信
2014/05/22 职场文书
党员自我评价2015
2015/03/03 职场文书
聋哑人盗窃罪辩护词
2015/05/21 职场文书
2016预备党员培训心得体会
2016/01/08 职场文书
导游词之宿迁乾隆行宫
2019/10/15 职场文书
开学季:喜迎新生,迎新标语少不了
2019/11/07 职场文书
Golang 遍历二叉树
2022/04/19 Golang
Python时间操作之pytz模块使用详解
2022/06/14 Python