At the same time, we know that in high-level object-oriented languages, creating objects containing private members is one of the most basic features, and providing properties and methods to access private members hides internal details. Although JS is also object-oriented, there is no internal mechanism to directly indicate whether a member is public or private. Again, relying on the language flexibility of JS, we can create public, private and privileged members. Information hiding is the goal we want to achieve, and encapsulation is the way we achieve this goal. Let's illustrate with an example: Create a class to store book data and display the data on a web page.
1. The simplest is to completely expose the object. Use constructors to create a class in which all properties and methods are accessible from the outside.
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;
}
The display method depends on whether the isbn is correct, if not you will not be able to get the image and link. With this in mind, an isbn must exist for each book, while the book's title and author are optional. On the surface, it seems that just specifying an isbn parameter can run normally. But it cannot guarantee the integrity of isbn. Based on this, we added isbn verification to make the book check more robust.
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;
}
};
We added checkIsbn() to verify the validity of the ISBN and ensure that display() can operate normally. However, the requirements have changed. Each book may have multiple versions, which means that the same book may have multiple ISBN numbers, and a separate algorithm for selecting versions needs to be maintained for control. At the same time, although the integrity of the data can be checked, it cannot control external access to internal members (such as assigning values to isbn, title, author), so there is no way to protect internal data. We continue to improve this solution and adopt interface implementation (providing get accessor/set memory).
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();
}
};
Note: In addition to the isbn, title, and author attributes that are marked as private members with "_", checkIsbn() is also marked as a private method.
3. Really privatize members through closures. If you are not familiar with the scope and nested functions in the closure concept, you can refer to the article "One of the Object-Oriented Javascripts (First Introduction to Javascript)", which will not be discussed in detail here.
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();
}
};
What are the differences between this solution and the previous one? First, use var in the constructor to declare three private members, and also declare the private method checkIsbn(), which is only valid in the constructor. Use the this keyword to declare a privileged method, that is, it is declared inside the constructor but has access to private members. Any method that does not need to access private members is declared in Book.prototype (such as display). That is, declaring methods that need to access private members as privileged methods is the key to solving this problem. However, this access also has certain drawbacks. For example, for each instance, a copy of the privileged method must be created, which will inevitably require more memory. We continue to optimize and use static members to solve the problems we face. By the way: static members only belong to the class, and all objects only share one copy (explained in "Object-oriented Javascript 2 (Implementing Interfaces), see Interface.ensureImplements method"), while instance methods are for objects. Word.
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)。这里我们继续考虑一个问题,假设以后我们需要对不同的书做限制,比如<
>最大印发量为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");
}
};
最完美的情况就是你所封装的程序对调用者而言,仅仅需要知道你的接口就可以,根本不关心你如何实现。但问题在于,随着工程量的扩大,你的封装内容必然会增大,在项目发生交接时,对于一个对作用域和闭包等概念不熟悉的成员来说,维护难度会变得如此之大。有些时候应需求响应必须改动源码(这里不一定指改接口),可能是新增一些细节,即使拿到你的源码却无从下手,那就不好做了。因此,我的建议:封装不要过度,接口一定要清晰,可扩展。