上一篇文章距离有出不多一个多月了,现在好不容易有了好心情,继续看书,写点感悟.
第三章讲述的是js封装,像java,可以通过private关键字来声明一个方法使得只有该对象内部的代码才能执行它,在js中没有这样的关键字,但是可以使用闭包来创建只允许从对象内部访问的方法和属性.相比于private,闭包走了一条弯路却达到同样的效果.
接口在js封装中的作用
上一篇文章讲到接口模式是许多其他js设计模式的基础,它定义了两个对象间的关系,接口不变那么关系的双方可以被替换,不一定非得使用像第二章那样严格的接口,而且应该避免公开定义于接口中的方法,否则其他对象可能会对那些并不属于接口的方法产生依赖,不安全,因为这些方法随时都可能改变或者被删除.一个理想软件系统应该未所有类定义接口,这些类只向外界提供他们实现的接口中规定的方法,任何别的方法都留作自用.其所有属性都是私有的,外界只能通过接口中定义的存取方法进行操作.
创建对象的基本模式
有3种,门户大开型只能提供公用成员;第二种,使用下划线来表示方法或者属性的私有性;第三种,通过闭包来创建真正私有的成员,这些成员只能通过特权方法访问.以Book 类为例 -- 一个用来存储关于一本书的数据的类,并为其实现一个以 HTML 形式显示这些数据的方法.现在只需创建这个类, 下面是其他人在创建并使用实例:
//Book(isbn, title, author)
var jsDesignPatterns = new Book('978-7-115-19128-1', 'JavaScript 设计模式', 'Harmes, R.');
jsDesignPatterns.display();
//Outputs the data by creatingand 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 必选, 因为 display 方法要求 book 对象都有一个准确的 isbn,否则就不能找到相应的图片,也不能生成一个用于购书的链接.title 和 author 参数都是可选的,要有默认值以防它们未被提供.
有个最大的问题,没有办法检查 isbn 数据的完整性,错误的 isbn 数据可能导致 display 方法失效.下面的版本强化了对 isbn 的检查:
var Book = function(isbn, title, author) {
if (!this.checkIsbn(isbn)) {
throw new Error('Book: Invalid 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) { // 10 digit ISBN.
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.
if (!isbn.match(/^\d{12}/)) { // Ensure characters 1 through 12 are digits.
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 false; // All tests passed.
},
display: function () {
...
}
};
checkIsbn 方法可以保证 ISBN 是一个具有正确位数和校验和的字符串.因为Book 类现在有两个方法,所以 Book.prototype 被设置为一个对象字面量,这样在定义多个方法的时候就不用在每个方法前面都加上Book.prototype.
现在保证了 display 方法可以正常工作,出现了另一个问题,假设一本书可能会有多个版本,每个版本都有自己的 ISBN,在实例化book对象之后直接修改 isbn 属性:
jsDesignPatterns.isbn = '978-0261103283';
jsDesignPatterns.display();
所以即使在构造器中对数据完整性进行检验,还是无法阻止其他人给 isbn赋值,为了保护内部数据,为每个属性提供 accessor 取值器和 mutator 赋值器方法.通过使用赋值器,你可以在把一个新值真正赋给属性之前进行各种检验.下面是加入了取值器合赋值器之后的新版 Book 对象:
var Publication = new Interface ('Publication', ['getIsbn', 'getTitle', 'setTitle', 'getAuthor', 'setAuthor', 'display']);
var Book = function (isbn, title, author) { // implements Publication
this.setIsbn(isbn);
this.setTitle(title);
this.setAuthor(author);
}
Book.prototype = {
checkIsbn: function (isbn) {
...
},
getIsbn: function () {
return this.isbn;
},
setIsbn: function (isbn) {
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 (author) {
this.author = author || 'No author specified';
},
diplay: function () {
...
}
};
上述代码定义了一个接口,只需要使用这个接口中定义的方法与对象来打交道.还有一些对数据有保护作用的取值器,赋值器方法,以及一些检验方法.
但是依然有缺陷:提供了赋值器方法,但那些属性仍然是公开的,可以被直接设置.而且无法保护内部数据,取值器和赋值器方法也引入了较多代码(js 文件大小较为重要).
用命名规范区别私用成员
依然是上面那个 snippet,只不过 setAttributes 的时候所有要设置的属性和方法都加上了 下划线_
前缀,表示它是私用属性和方法(js 中可以使用下划线和字母开头命名出有效变量).
下划线的这种用法表明一个属性或者方法仅供对象内部使用,直接访问它或者设置它可能会导致意想不到的后果,这有助于防止程序员对它的无意使用,却不能防止有意使用使用.
这个规范只有在得到遵守时才有效果,并不是真正可以用来隐藏对象内部数据的解决方案,主要适用于非敏感性的内部方法和属性.
作用域,嵌套函数和闭包
在讨论这种真正的私用性方法和属性的实现技术之前,我们先巩固一下相关基础.在 js 中,只有函数具有作用域,在一个函数内部声明的变量在函数外部无法访问,私用属性也是希望无法在对象外部访问的变量,所以作用域相关性明显.定义在一个函数中的变量在该函数中的内嵌函数是可以访问的.
function foo() {
var a = 10;
function bar() {
a *= 2;
}
bar();
return a;
}
a定义在函数foo中,但函数 bar 可以访问它,因为 bar 也定义在 foo 中,bar 内部对 a 赋新值,当 bar 在 foo 中被调用时它能够访问 a,这可以理解.如果 bar 在 foo 外部被调用呢
function foo() {
var a = 10;
function bar () {
a *= 2;
return a;
}
return bar;
}
var baz = foo(); // baz is now a reference to functionbar.
baz(); // return 20;
baz(); // return 40;
baz(); // return 80;
var blat = foo(); // blat is another reference to bar.
blat(); // return 20, because a new copy of a is being used.
在上述代码中, 所返回的对 bar 函数的引用被赋给变量 baz.这个函数现在是在 foo 外部被调用,但他依然能够访问 a.这是因为 js 中的作用域是词法性的,函数是运行在定义他们的作用域中(本例中是 foo 内部的作用域),而不是运行在调用他们的作用域中,只要 bar 被定义在 foo 中,他就能访问在 foo 中定义的所有变量, 即时 foo 的执行已经结束.
在 foo 返回后,它的作用域被保存下来,但是只有他返回的那个函数能够访问这个作用域.baz和 blat 各有这个作用域及 a 的一个副本,而且只有他们自己能对其进行修改.返回一个内嵌函数是创建闭包最常用的手段.
用闭包实现私用成员
现在有了对闭包的理解之后再来讨论一下我们刚刚要做的事: 需要创建一个只能在对象内部访问的变量.借助于闭包你可以创建只允许特定函数访问的变量,而且这些变量在这些函数的各次调用之间依然存在.为了创建私用属性,你需要在构造器函数的作用域中定义相关变量,这些变量可以被定义于该作用域中的所有函数访问,包括哪些特权方法:
var Book = function(newIsbn, newTitle, newAuthor){ // implements Punlication
// Private attribute.
var isbn, title, author;
// Private method.
function checkIsbn(isbn) {
...
}
// Privilleged 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 () {
...
}
};
以上代码和之前的创建对象模式有什么不同呢,其他情况下我们在创建和引用对象的属性时总要使用 this 关键字.但是这个地方我们用 var 声明这些属性变量,它们只存在于Book 构造器中,checkIsbn 函数是私有方法.
需要访问这些变量和函数的方法只需声明在 Book 中,被称作特权方法(privileged method),他们是公有方法,之所以有 特权方法前面都用 this 关键词来帮助声明, 是为了在对象外部能够被访问.这些方法定义于 Book 构造器的作用域中,所以它们能够访问到私有属性,引用这些属性时并没有使用 this关键词,因为他们没有公开.所有取值器和赋值器方法都被改为不加 this 的直接引用这些属性.
任何不需要直接访问私用属性的方法都可以像原来那样在 Book.prototype中声明.display 就是这类方法中的一个,他可以通过调用 getIsbn或者 getTitle等等特权方法来间接访问任何私用属性.只有那些需要直接访问私用成员的方法才是特权方法.需要注意的是,特权方法太多会占用较多内存.每个对象实例都包含了所有特权方法的新副本.
用闭包方式创建的对象可以具有真正的私有属性,其他程序员不可能直接访问它们创建的Book 实例的任何内部数据.
但是,,,这样使用闭包还是有缺点的.之前的门户大开型对象创建模式中,所有方法都创建在原型对象中,因此不管生成多少对象实例,这些方法在内存中只存在一份.而在本节中每生成一个新的对象实例都将 copy 每一个私有方法和特权方法,,,这样就会带来更多内存消耗,所以只适合用在真正私有成员的场合.这种对象创建模式也不利于派生子类,因为所派生出的子类不能访问超类的任何私有属性或者方法.
所以在 js 中用闭包实现私有成员导致的派生问题被称作**"继承破坏封装(inheritance breaks encapsulation)",如果你创建的类以后可能会需要派生出子类, 那么最好还是采用前两种对象创建模式.
高级的对象创建模式
静态方法和属性
刚才讲的作用域和闭包可用于创建静态成员(公有和私有),大多数方法和属性所关联的是类的实例,但是静态成员所关联的是类本身.也就是说,静态成员是在类的层次上操作,不是实例层次上.每个静态成员都只有一份,是通过类对象方法的.
下面是添加了静态属性和方法和 Book 类:
var Book = (function () {
// Private static attributes.
var numOfBooks = 0;
// Private static method.
function checkIsbn(isbn) {
...
}
// Return the constructor.
return function(newIsbn, newTitle, newAuthor) { // implements Publication
// Private attributes.
var isbn;
var title;
var 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) {
title = newAuthor || 'No title specified';
};
// Constructor code.
numOfBooks++; // Keep track of how many Books have been instantiated with the private static attribute.
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(inputString) {
...
};
// Public, non-privileged methods.
Book.prototype ={
display: function () {
...
}
};
与前一节创建的类大题相似,但是有重要区别.这里的私有成员和特权方法仍然声明在构造器中(分别使用 var 和 this 关键字),但是那个构造器却从原来的普通函数办成了一个内嵌函数,并且被作为包含它的函数的返回值赋给变量 Book.这里创建了一个闭包,里面声明了静态的私有成员.位于外层函数声明之后的一对空括号很重要 -- 代码一载入就立即执行这个函数(而不是在调用 Book 构造函数时),这个函数的返回值是另一个函数,被赋值给 Book 变量 -- 一个构造函数,在实例化 Book 时,所调用的是这个内层函数,外层函数只是用于创建一个可以用来存放静态私有成员的闭包.
私有的静态成员可以从构造器内部访问,这意味着所有私有函数和特权函数都能访问它们.与其他方法相比,他们在内存中只会存放一份.
因为它们在构造器之外,所以不是特权方法,不能访问任何定义在构造器中的私有属性.定义在构造器中的私有方法能够调用那些私有静态方法.
要判断一个私有方法是否应该被设计成静态方法,主要看它是否需要访问任何实例数据,如果不需要那么设计成静态方法更省内存.
常量
在 js 中,可以通过创建只有取值器而没有赋值器的私有变量来模仿常量,而且不因对象实例的不同而变化,所以将其作为私有静态属性来设计是合乎情理的.
假设 Class 对象有一个 UPPER_BOUND的常量,那么为了获取这个常量而进行的方法调用Class.getUPPER_BOUND();
为了实现这个取值器,需要使用特权静态方法:
var Class = (function() {
// Constants (created as private static attributes).
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 attributes.
var constants = {
UPPER_BOUND: 100,
LOWER_BOUND: -100
};
// Constructor.
var ctor = function(constructorArgument) {
...
};
// Privileged static method.
ctor.getConstant = function(name) {
return constants[name];
}
...
// Return the constructor.
return ctor;
})();
单体和对象工厂
这两个模式就是使用闭包来创建受保护的变量空间.后面会慢慢涉及,在此先简要介绍一下,单体模式使用一个外层函数返回的对象字面量来公开特权成员,而私用成员则被保护性地封装在外层函数的作用域中,主要原理是:外层函数在定义后立即执行,其结果被赋给一个变量.前面的例子中外层函数返回的都是一个函数,而单体模式中外层函数返回的是一个对象字面量.
对象工厂可以使用闭包来创建具有私有成员的对象.最简形式是一个类构造器.
封装的好处
保护了内部数据的完整性,通过讲数据的访问途径限制为取值器和赋值器这两个方法,可以获得对取值和赋值的完全控制.这样可以减少其他函数所需要的错误检查代码数量,并且确保数据不会处于无效状态.另外,对象的重构可以变得更轻松.因为用户不知道对象的内部细节,所以可以随心所欲的修改对象内部使用的数据结构和算法.
通过只公开那些在接口中规定的方法,可以弱化模块间的耦合.尽可能的提高对象的独立性可以带来许多好处:提高对象的可重用性,使其在必要的时候可以被替换.使用私有变量可以避免空间冲突,如果一个变量在代码中其他地方都不能被访问,就不用担心它是否与程序中其他地方的对象或者函数重名,大幅改动对象的内部细节也不会影响其他代码.
坏处
封装也存在一定的缺憾.比如,私有方法很难进行单元测试.因为他们还有其内部变量都是私有的,在对象外部没法访问.
要么通过使用公有方法来提供访问途径(这样就市区许多私有方法的好处),要么设法在对象内部定义并执行所有单元测试.最好的解决办法是只对公有办法进行单测.可以覆盖到所有私有方法,但是却是间接的.这种问题不是 js 特有的,只对公有方法进行单测较易接受作用域链复杂的话可能会使错误调试更加困难,有时候很难区分来自不同作用域的许多同名变量.此问题不是经过封装的对象所特有的,但是实现私有方法和属性所有的闭包会让它变得更加复杂.
过度封装可能会损害类的灵活性,不利于和小伙伴之间的合作,他可能对你的类的需求了解的并不透彻.
-最大的问题在于 js 实现封装较为困难.不利于新手使用.js 与大多数面向对象语言不同,封装涉及的调用链和定义后立即执行的匿名函数等概念加大了学习难度.
小结
本文讨论了信息隐藏的概念以及如何使用封装这种手段来实现它,js 没有对封装提供内置的支持,所以需要依赖其他东西.
如果可以确信其他小伙伴只会使用接口中规定的方法,或者并非迫切需要保持内部数据的完整性,那,那么可以使用门户大开型对象.
命名规范用来告知小伙伴哪些方法是不宜直接方法的内部方法.如果需要真正的私有成员,那么只能使用闭包.通过创建一个受保护的变量空间,可以实现公有,私有和特权成员,静态成员,常量.理解 js 作用域的特点,可以模仿出各种面向对象技术.