面向对象的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 相关文章推荐
15个jquery常用方法、小技巧分享
Jan 13 Javascript
AngularJS基础 ng-src 指令简单示例
Aug 03 Javascript
微信JS-SDK选取手机照片上传功能
Apr 21 Javascript
基于JavaScript实现微信抢红包功能
Jul 20 Javascript
js精确的加减乘除实例
Nov 14 Javascript
VUE element-ui 写个复用Table组件的示例代码
Nov 18 Javascript
Vue Socket.io源码解读
Feb 07 Javascript
js实现时分秒倒计时
Dec 03 Javascript
深入分析jQuery.one() 函数
Jun 03 jQuery
JavaScript日期库date-fn.js使用方法解析
Sep 09 Javascript
swiper实现导航滚动效果
Dec 13 Javascript
JS新手入门数组处理的实用方法汇总
Apr 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循环输出数据库内容的代码
2008/05/24 PHP
thinkphp使用literal防止模板标签被解析的方法
2014/11/22 PHP
PHP crypt()函数的用法讲解
2019/02/15 PHP
jquery checkbox全选、取消全选实现代码
2010/03/05 Javascript
自己整理的一个javascript日期处理函数
2010/10/16 Javascript
jQuery之end()和pushStack()使用介绍
2012/02/07 Javascript
javascript实现颜色渐变的方法
2013/10/30 Javascript
js实现简单折叠、展开菜单的方法
2015/08/28 Javascript
三分钟带你玩转jQuery.noConflict()
2016/02/15 Javascript
微信小程序  modal详解及实例代码
2016/11/09 Javascript
JS实现unicode和UTF-8之间的互相转换互转
2017/07/05 Javascript
微信小程序授权获取用户详细信息openid的实例详解
2017/09/20 Javascript
three.js中文文档学习之创建场景
2017/11/20 Javascript
Vue-Router模式和钩子的用法
2018/02/28 Javascript
vue引用js文件的多种方式(推荐)
2018/05/17 Javascript
全面解析vue router 基本使用(动态路由,嵌套路由)
2018/09/02 Javascript
小程序实现页面顶部选项卡效果
2018/11/06 Javascript
Vue中用props给data赋初始值遇到的问题解决
2018/11/27 Javascript
react脚手架如何配置less和ant按需加载的方法步骤
2018/11/28 Javascript
vue form check 表单验证的实现代码
2018/12/09 Javascript
webpack4 从零学习常用配置(小结)
2019/05/28 Javascript
浅谈Node新版本13.2.0正式支持ES Modules特性
2019/11/25 Javascript
python字符类型的一些方法小结
2016/05/16 Python
python实现unicode转中文及转换默认编码的方法
2017/04/29 Python
tensorflow中tf.slice和tf.gather切片函数的使用
2020/01/19 Python
关于Theano和Tensorflow多GPU使用问题
2020/06/19 Python
Python3如何使用多线程升程序运行速度
2020/08/11 Python
HTML5中的Article和Section元素认识及使用
2013/03/22 HTML / CSS
台湾最大银发乐活百货:乐龄网
2018/05/21 全球购物
ManoMano英国:欧洲第一家专注于DIY和园艺市场的电商平台
2020/03/12 全球购物
无刑事犯罪记录证明范本
2014/09/29 职场文书
党员群众路线剖析材料
2014/10/08 职场文书
公司向个人借款协议书范本
2014/10/09 职场文书
小学感恩节活动总结
2015/03/24 职场文书
二十年同学聚会感言
2015/07/30 职场文书
java Nio使用NioSocket客户端与服务端交互实现方式
2021/06/15 Java/Android