封装和信息隐藏
先说几个概念:
私有方法
- 可以访问类的所有属性,包括公有属性和私有属性。
- 但是不可以在类的外部调用。
公有方法
- 可以在类的外部调用
- 不可以访问类的私有属性
- 公有方法必须在类的内部或者外部通过 prototype属性添加
特权方法
- 可以在类的外部调用
- 可以访问私有属性和公有属性,可以认为是一张特殊的公有方法
- 特权方法必须在类的内容声明定义。
静态方法
- 属于类本身的方法
- 对象实例不能调用对象的静态方法
信息隐藏原则
封装与信息隐藏
封装 可以被定义为对对象的内部数据表现形式和实现细节进行隐藏。
创建对象的基本模式
JavaScript中创建对象的基本模式有3种:
第一种 门户大开型最简单的一种方式,但只提供公共成员。
第二种 使用下划线来表示方法和属性的私用性。
第三种 使用闭包来创建真正的私用成员,这些成员只能通过一些特权方法。
以Book类为例,假设你接到一项任务,用来储存关于一本书的数据的类,并为其实现一个以HTML形式显示这些数据的方法。
// Book(isbn, title, author)
var theHobbit = new Book("0-395-07122-4", "The Hobbit", "J.R.R.Tolkien ");
theHobbit.display(); // Outputs the data by creating and populating an HTML element.
门户大开型对象
实现 Book类最简单的做法是按传统方式创建一个类,用一个类来做其构造器。我们称其为门户大开型对象,因为所有属性和方法都是公开的、可访问的。这些公有属性使用 this关键字创建:
var Book = function(isbn, title, author){
if(isbn == undefined) throw new Error("Book constructor requires an isbn.");
this.isbn = isbn;
this.title = title || "No title specified";
this.author = author || "No author specified";
}
Book.prototype.display = function(){
...
};
以上这段代码,最大的问题就是无法检验 ISBN数据的完整性,而不完整的 ISBN数据有可能导致 display方法失灵,下面版本强化了对 ISBN的检查:
var Book = function(isbn, title, author){
if(isbn == undefined) throw new Error("Book constructor requires an isbn.");
this.isbn = isbn;
this.title = title || "No title specified";
this.author = author || "No author specified";
}
Book.prototype = {
checkIsbn : function(isbn){
if(isbn == undefined || typeof isbn != "string"){
return false;
}
isbn = isbn.replace(/-/, ""); // Remove dashes.
if(isbn.length != 10 && isbn.length != 13){
return false;
}
var sum = 0;
if(isbn.length === 10){
if(!isbn.match(/^\d{9}/)){ // Ensure characters 1 through 9 are digits 确保是数字
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 { // 13 digit ISBN
...
}
return true; // All tests passed;
},
display : function(){
...
}
};
下面是加入取值器和赋值器的新版Book对象,方便控制 isbn属性。
var Publication = new Interface("Publiccation", ["getIsbn", "setIsbn", "getTitle", "setTitle", "getAuthor", "setAuthor", "display"]);
var Book = function(isbn, title, author){
this.setIsbn(isbn);
this.setTitle(title);
this.setAuthor(author);
}
Book.prototype = {
checkIsbn : function(isbn){
...
},
getIsbn : function(){
return this.isbn;
},
setIsbn : function(){
if(!this.checkIsbn(isbn)) throw new Error("Book : Invalid ISBN");
this.isbn = isbn;
},
getTitle : function(){
return this.title;
},
setTitle : function(title){
this.title = title || "No title specified";
},
getAuthor : function(){
return this.author;
},
setAuthor : function(){
this.author = author || "No author specified";
},
display : function(){
...
}
};
这种门户大开型创建对象的方式,
优点:易于使用,方法和属性都是公开的,派生子类和进行单元测试很方便;
缺点:无法保护内部数据。
用命名规范区别私有成员
使用命名规范来模仿私用成员的模式,与门户大开型类似,只是在一些方法前加“_”来表示私用性。
var Book = function(isbn, title, author){
if(isbn == undefined) throw new Error("Book constructor requires an isbn.");
this.isbn = isbn;
this.title = title || "No title specified";
this.author = author || "No author specified";
}
var Publication = new Interface("Publiccation", ["getIsbn", "setIsbn", "getTitle", "setTitle", "getAuthor", "setAuthor", "display"]);
var Book = function(isbn, title, author){
this.setIsbn(isbn);
this.setTitle(title);
this.setAuthor(author);
}
Book.prototype = {
_checkIsbn : function(isbn){
...
},
getIsbn : function(){
return this._isbn;
},
setIsbn : function(){
if(!this._checkIsbn(isbn)) throw new Error("Book : Invalid ISBN");
this._isbn = isbn;
},
getTitle : function(){
return this._title;
},
setTitle : function(title){
this._title = title || "No title specified";
},
getAuthor : function(){
return this._author;
},
setAuthor : function(){
this._author = author || "No author specified";
},
display : function(){
...
}
};
下划线表明,一个属性(或方法)仅供对象内部使用,直接访问它或者设置它可能会导致意想不到的后果。
作用域、嵌套函数和闭包
在JavaScript中,只有函数具有作用域,在函数内容声明的变量在函数外部无法访问。私用属性就其本质而言在对象外部无法访问。定义在函数中的变量在函数内嵌函数中可以访问。比如:
function foo(){
var a = 10;
function bar(){
a *= 2;
}
bar();
return a;
}
var baz = foo(); // baz现在引用 bar;
baz(); // 返回20
baz(); // 返回40
baz(); // 返回80
var blat = foo(); // blat是 bar的另一个引用
blaz(); // 返回20, 因为这是 bar的一个新的复制
用闭包实现私用成员
借助闭包可以创建只允许特定函数访问的变量,并且这些变量在这些函数的各次调用之间仍然存在。
为了创建私用变量,你需要在构造器函数的作用域中定义相关变量,这些变量可以被定义于该作用域中的所有函数访问,包括特权方法:
var Book = function(newIsbn, newTitle, new Author){
// Private attributes.
var isbn, title, author;
// Private methods.
function checkIsbn(isbn){
...
}
// Privileged methods.
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 || "No title specified";
};
this.getAuthor = function(){
return author;
};
this.setAuthor = function(newAuthor){
author = newAuthor || "No author specified";
};
// Constructor code.
this.setIsbn(newIsbn);
this.setTitle(newTitle);
this.setAuthor(newAuthor);
};
// Public, non-privileged methods.
Book.prototype = {
display : function(){
...
}
};
更多高级对象创建模式
静态方法和属性
作用域和闭包的概念可用于创建静态成员,包括公用的和私用的。大多数方法和属性所关联的是类的实例,而静态成员所关联的是类本身。静态成员是直接通过类对象访问。
下面是添加了静态属性和方法的Book类:
var Book = (function(){
// Private static attributes.
var numOfBooks = 0;
// Private static method.
function checkIsbn(isbn){
...
}
// Return the constructor.
return function(newIsbn, newTitle, newAuthor){
// Private attributes.
var isbn, title, author;
// Privileged methods.
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 || "No title specified";
};
this.getAuthor = function(){
return author;
};
this.setAuthor = function(newAuthor){
author = newAuthor || "No author specified";
};
// Constructor code.
numOfBooks++; // 通过公有静态属性来监听 Book被实例化的数量
if(numOfBooks > 50) throw new Error("Book : Only 50 instances of Book can be " + " created.");
this.setIsbn(newIsbn);
this.setTitle(newTitle);
this.setAuthor(newAuthor);
}
})();
// Public static method.
Book.convertToTitleCase = function(){
...
};
// Public, non-privileged methods.
Book.prototype = {
display : function(){
...
}
};
常量
常量是一些不能被修改的变量,在JavaScript中,可以通过创建只有取值器没有赋值器的私用变量来模仿常量。
假设Class对象有一个名为 UPPER_BOUND的常量,为获取这个常量而进项的方法调用如下:
Class.getUPPER_BOUND();
// 为实现这个取值器,需要使用特权静态方法
var Class = (function(){
// 常量(把它当成私有静态属性创建)
var UPPER_BOUND = 100;
// Constructor.
var ctor = function(constructorArgument){
..
}
// Privileged static method.
ctor.getUPPER_BOUND = function(){
return UPPER_BOUND;
};
...
// Return the constructor
return ctor;
})();
如果需要很多常量,可以创建一个通用的取值器方法:
var Class = (function(){
// Private static attribute.
var constants = {
UPPER_BOUND : 100,
LOWER_BOUND : -100
};
// Constructor
var ctor = function(){
...
};
ctor.getConstants = function(name){]
return constants[name];
};
...
// Return the constructor
return ctor;
})();
// 然后通过使用这个取值器以或得一个常量
Class.getConstant("UPPER_BOUND");
单体和工厂模式
单体模式 使用一个由外部函数返回的对象字面来公开特权成员,私用成员封装在外层函数的作用域中。前面的例子都返回的是一个函数,单体模式外层函数返回的是一个对象字面量。
工厂模式 使用闭包来创建具有私用成员的对象。最简形式就是一个类构造器。