在"初识Javascript"章节中,提到通过作用域和闭包来隐藏内部细节,并给外部提供访问接口,就初次接触到了信息隐藏的概念了。同时,我们知道在面向对象的高级语言中,创建包含私有成员的对象是最基本的特性之一,提供属性和方法对私有成员进行访问来隐藏内部的细节。虽然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");
}
};
最完美的情况就是你所封装的程序对调用者而言,仅仅需要知道你的接口就可以,根本不关心你如何实现。但问题在于,随着工程量的扩大,你的封装内容必然会增大,在项目发生交接时,对于一个对作用域和闭包等概念不熟悉的成员来说,维护难度会变得如此之大。有些时候应需求响应必须改动源码(这里不一定指改接口),可能是新增一些细节,即使拿到你的源码却无从下手,那就不好做了。因此,我的建议:封装不要过度,接口一定要清晰,可扩展。