面向对象的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 相关文章推荐
Extjs中DisplayField的日期或者数字格式化扩展
Sep 03 Javascript
eval与window.eval的差别分析
Mar 17 Javascript
javascript 小数取整简单实现方式
May 30 Javascript
jQuery树形下拉菜单特效代码分享
Aug 15 Javascript
web 屏蔽BackSpace键实例代码
Dec 24 Javascript
ExtJs异步无法向外传值和赋值的完美解决办法
Jun 14 Javascript
jQuery使用bind函数实现绑定多个事件的方法
Oct 11 jQuery
[原创]jQuery实现合并/追加数组并去除重复项的方法
Apr 11 jQuery
详解Vue前端对axios的封装和使用
Apr 01 Javascript
详解vue父子组件关于模态框状态的绑定方案
Jun 05 Javascript
小程序Request的另类用法详解
Aug 09 Javascript
three.js利用射线Raycaster进行碰撞检测
Mar 12 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
mysql建立外键
2006/11/25 PHP
web站点获取用户IP的安全方法 HTTP_X_FORWARDED_FOR检验
2013/06/01 PHP
在PHP中使用redis
2013/11/04 PHP
用 Composer构建自己的 PHP 框架之设计 MVC
2014/10/30 PHP
php数组去除空值函数分享
2015/02/02 PHP
php将金额数字转化为中文大写
2015/07/09 PHP
PHP 微信支付类 demo
2015/11/30 PHP
Jquery中的层次选择器与find()的区别示例介绍
2014/02/20 Javascript
Javascript的&amp;&amp;和||的另类用法
2014/07/23 Javascript
JavaScript如何实现对数字保留两位小数一位自动补零
2015/12/18 Javascript
JavaScript实现自动切换图片代码
2016/10/11 Javascript
解决vue2.x中数据渲染以及vuex缓存的问题
2017/07/13 Javascript
ES6知识点整理之对象解构赋值应用示例
2019/04/17 Javascript
js实现GIF图片的分解和合成
2019/10/24 Javascript
[03:11]完美世界DOTA2联赛PWL DAY8集锦
2020/11/09 DOTA
Python使用scrapy采集数据过程中放回下载过大页面的方法
2015/04/08 Python
简单谈谈Python中的元祖(Tuple)和字典(Dict)
2017/04/21 Python
python 定时修改数据库的示例代码
2018/04/08 Python
对Python中for复合语句的使用示例讲解
2018/11/01 Python
Pandas Shift函数的基础入门学习笔记
2018/11/16 Python
python实现归并排序算法
2018/11/22 Python
python实现LBP方法提取图像纹理特征实现分类的步骤
2019/07/11 Python
Python matplotlib绘制饼状图功能示例
2019/09/10 Python
Pytorch高阶OP操作where,gather原理
2020/04/30 Python
Python web框架(django,flask)实现mysql数据库读写分离的示例
2020/11/18 Python
发现世界上最好的珠宝设计师:JewelStreet
2017/12/17 全球购物
SEPHORA丝芙兰捷克官网:购买香水、化妆品和护肤品
2018/11/26 全球购物
ECOSUSI官网:女式皮革背包
2019/09/27 全球购物
个人求职信范文分享
2013/12/13 职场文书
材料员岗位职责
2014/03/13 职场文书
语文高效课堂实施方案
2014/05/03 职场文书
办理信用卡工作证明
2014/09/30 职场文书
购房协议书范本
2014/10/02 职场文书
2019年汽车租赁合同范本!
2019/08/12 职场文书
使用pandas模块实现数据的标准化操作
2021/05/14 Python
JS前端可视化canvas动画原理及其推导实现
2022/08/05 Javascript