深入js的面向对象学习篇(封装是一门技术和艺术)——温故知新(二)
下面全面介绍封装和信息隐藏。
通过将一个方法或属性声明为私用的,可以让对象的实现细节对其它对象保密以降低对象之间的耦合程度,可以保持数据的完整性并对其修改方式加以约束。在代码有许多人参与设计的情况下,这也可以使代码更加可靠、更易于调试。
不像其它语言,javascript中没有类似private这种关键字。我们将使用闭包的概念来创建只允许从对象内部访问的方法和属性。
封装之利:封装保护了内部数据的完整性。通过将数据的访问途径限制为取值器和赋值器这两个方法,可以获得对取值和赋值的完全控制。通过只有公开那些在接口规定的方法,可以弱化模块间的耦合。使用私用变量也有助于避免命名空间冲突。如果一个变量在代码中其他地方都不能被访问,你就不用老担心它是否与程序中其它地方的对象或函数重名并因此造成问题。
封装弊端:很难进行单元测试。过度封装降低灵活性。封装是一门技术活,也是一门艺术活。
创建对象的基本模式:
① 门户大开型对象
所有属性和方法都是公开的、可访问的。
var Book = function(isbn,title,author) { if(isbn == undefined) throw new Error("...."); this.isbn = isbn; this.title = title||'No title specified'; this.author = author||'No author specified'; } Book.prototype.display = function() { .... }; //增强版还可以是这样的:将Book.prototype设为一个对象字面量 Book.prototype = { checkIsbn: function(isbn) { .... }, display: function() { ... } }; //再次增强版,为了保护内部的数据结构,为每个属性都提供了取值器和赋值器方法 Book.prototype = { checkIsbn: function(isbn) { .... }, getIsbn: function() { return this.isbn; }, setIsbn: function(isbn) { if(!checkIsbn(isbn)) throw new Error(".."); this.isbn = jsbn; }, getTitle: function() { return this.title; }, setTitle: function() { this.title = title||"No title specified"; }, .... display: function() { .... } };
需要明确的是:虽然我们为设置属性提供了赋值器方法,但那些属性仍然是公开的,可以被直接设置,而在这种方案中却无法阻止这种行为。上述方法的简单性可嘉,却无法避免内部数据被破坏。怎么改进呢?
改进:
a. 使用命名规范区别私用变量,比如采用下划线命名规范,表明一个属性(或方法)仅供对象内部使用:道理很简单,用命名方式提醒对方,这是一个私有量,但是愿望是美好的,事实未必如你所愿。
b. 利用作用域、嵌套函数、和闭包:javascript中,只有函数具有作用域。
function foo() { var a = 10; function bar() { a *= 2; return a; } return bar; } var baz = foo(); baz(); //20 baz(); //40 baz(); //80 //闭包实现私有成员 var Book = function(newIsbn,newTitle,newAuthor) { //私有属性 var isbn,title,author; //这也再次证明了this.isbn实际上绑定到了window上,如果不是通过new方法来实例对象的话 //私有方法 function checkIsbn(isbn) { ...... } //特权方法 this.getIsbn = function() { return isbn; }; this.setIsbn = function(newIsbn) { if(!checkIsbn(newIsbn)) throw new Error(".."); isbn = newIsbn; }; ..... //构造函数代码 this.setIsbn(newIsbn); this.setTitle(newTitle); }; //公共方法,不是特权方法 Book.prototype = { display: function() { .... } };
说明:对于特权方法我们都加了关键字this,因为这些方法都定义在Book构造器的作用域中,所以可以访问私有属性。
任何不需要直接访问私用属性的方法都可以像原来那样在Book.prototype中声明。它不需要直接访问任何私有属性,因为它可以通过调用getIsbn()来进行间接访问。只有那些需要直接访问私有成员的方法才应该被设计为特权方法。但特权方法太多又会占用过多内存,因为每个对象实例都包含了所有特权方法的副本。
② 更多高级对象创建模式
利用静态方法和属性,消除副本。
大多数方法和属性所关联的是类的实例,而静态成员所有关联的则是类本身。换句话说,静态成员是在类的层次上操作,而不是在实例的层次上操作。每个静态成员是直接通过类对象访问。
var Book = (function() { //私有静态成员 var numOfBooks = 0; //私有静态方法 function checkIsbn(isbn) { .... } //返回构造器 return function(newIsbn,newTitle,newAuthor) { //私有属性 var isbn,title,author; //私有方法 function checkIsbn(isbn) { ...... } //特权方法 this.getIsbn = function() { return isbn; }; this.setIsbn = function(newIsbn) { if(!checkIsbn(newIsbn)) throw new Error(".."); isbn = newIsbn; }; ..... //构造函数代码 numOfBooks++; this.setIsbn(newIsbn); this.setTitle(newTitle); } })(); //看到这对括号没^_^想想是什么用意吧 //公有静态方法 Book.convertTotitleCase = function(inputString) { .... }; //公有非特权方法 Book.prototype = { display: function() { .... } };
在实例化Book时,所调用的是这个内层函数,外层那个函数只是用于创建一个可以用来存放静态私用成员的闭包。
checkIsbn被设计成静态方法。因为为Book的每个实例都生成这个方法的一新副本毫无道理。
要判断一个私用方法是否应该被设计成静态方法,经验就是:看它是否需要访问任何实例数据。如果它不需要要,那么就将其设计为静态方法更为高效。
温馨提醒:别看例子看晕了,如果用一下印象会更深刻。~话说,你还记得那对括号吗?就是Book定义完成后那对括号。你知道封装好的对象是怎么运行怎么用的吗?且看下面的例子,运行一下,输出结果,一目了然:
<!doctype html> <html> <head> <script> var Book = (function() { //私有静态成员 var numOfBooks = 0; //私有静态方法 function checkIsbn(isbn) { } //返回构造器 return function(newIsbn,newTitle,newAuthor) { //私有属性 var isbn,title,author; //私有方法 function checkIsbn(isbn) { return true; //测试用 } //特权方法 this.getIsbn = function() { return isbn; }; this.setIsbn = function(newIsbn) { if(!checkIsbn(newIsbn)) throw new Error(""); isbn = newIsbn; alert(isbn); //测试用 }; //构造函数代码 numOfBooks++; this.setIsbn(newIsbn); } })(); //看到这对括号没^_^想想是什么用意吧 //公有静态方法 Book.convertTotitleCase = function(inputString) { }; //公有非特权方法 Book.prototype = { display: function() { } }; var ex1 = Book("001","001","001"); //测试用 var ex2 = Book("002","002","002"); //测试用 </script> </head> <body> </body> </html>