面向对象的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 相关文章推荐
用prototype实现的简单小巧的多级联动菜单
Mar 24 Javascript
DWR Ext 加载数据
Mar 22 Javascript
利用JS解决ie6不支持max-width,max-height问题的方法
Jan 02 Javascript
jquery轮播的实现方式 附完整实例
Jul 28 Javascript
bootstrap实现动态进度条效果
Mar 08 Javascript
Angularjs 与 bower安装和使用详解
May 11 Javascript
JQuery 选择器、DOM节点操作练习实例
Sep 28 jQuery
jQuery中extend函数简单用法示例
Oct 11 jQuery
vue视频播放插件vue-video-player的具体使用方法
Nov 08 Javascript
国内常用的js类库大全(CDN公共库)
Jun 24 Javascript
vue 实现上传组件
May 31 Vue.js
AngularJS实现多级下拉框
Mar 25 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
全国FM电台频率大全 - 22 重庆市
2020/03/11 无线电
PHP 文件上传源码分析(RFC1867)
2009/10/30 PHP
ThinkPHP之R方法实例详解
2014/06/20 PHP
PHP实现批量生成App各种尺寸Logo
2015/03/19 PHP
php基础设计模式大全(注册树模式、工厂模式、单列模式)
2015/08/31 PHP
javascript 模拟JQuery的Ready方法实现并出现的问题
2009/12/06 Javascript
过虑特殊字符输入的js代码
2010/08/05 Javascript
javascript来定义类的规范小结
2010/11/19 Javascript
JavaScript中判断对象类型的几种方法总结
2013/11/11 Javascript
使用HTML+CSS+JS制作简单的网页菜单界面
2015/07/27 Javascript
JavaScript简单修改窗口大小的方法
2015/08/03 Javascript
详解HTML5 使用video标签实现选择摄像头功能
2017/10/25 Javascript
Angular 向组件传递模板的两种方法
2018/02/23 Javascript
jquery实现的简单轮播图功能【适合新手】
2018/08/17 jQuery
angular ng-model 无法获取值的处理方法
2018/10/02 Javascript
JavaScript获取页面元素的常用方法详解
2019/09/28 Javascript
Vue中通过属性绑定为元素绑定style行内样式的实例代码
2020/04/30 Javascript
简单的通用表达式求10乘阶示例
2014/03/03 Python
Python获取暗黑破坏神3战网前1000命位玩家的英雄技能统计
2016/07/04 Python
Python 登录网站详解及实例
2017/04/11 Python
利用python编写一个图片主色转换的脚本
2017/12/07 Python
python opencv实现任意角度的透视变换实例代码
2018/01/12 Python
Python线性回归实战分析
2018/02/01 Python
python 处理微信对账单数据的实例代码
2019/07/19 Python
分享一个页面平滑滚动小技巧(推荐)
2019/10/23 HTML / CSS
HTML5 Canvas 起步(1) - 基本概念
2009/05/12 HTML / CSS
HTML5打开手机扫码功能及优缺点
2017/11/27 HTML / CSS
使用canvas生成含有微信头像的邀请海报没有微信头像问题
2019/10/29 HTML / CSS
七年级英语教学反思
2014/01/15 职场文书
计算机学生的自我评价分享
2014/02/18 职场文书
社会实践的活动方案
2014/08/22 职场文书
golang中的空接口使用详解
2021/03/30 Python
pytorch实现线性回归以及多元回归
2021/04/11 Python
mybatis使用oracle进行添加数据的方法
2021/04/27 Oracle
Python实现老照片修复之上色小技巧
2021/10/16 Python
MyBatis自定义SQL拦截器示例详解
2021/10/24 Java/Android