在JavaScript中,只有函数具有作用域。也就是说,在一个函数内部声明的变量在函数外部无法访问。私有属性就其本质而言就是你希望在对象外部无法访问的变量,所以为实现这种拒访性而求助于作用域这个概念是合乎情理的。定义一个函数中的变量在该函数的内嵌函数中是可以访问的。下面这个示例说明了JavaScript中作用域的特点:
function foo(){
var a = 10;
fucntion bar(){
a *= 2;
}
bar();
return a;
}
在这个示例中,a定义在函数foo中,但函数bar可以访问它,因为bar也定义在foo中。bar在执行过程中将a设置为 a * 2 。当bar在foo中被调用时它能够访问a,这可以理解。但是如果bar是在foo外部被调用的呢?
function foo(){
var a = 10;
function bar(){
a *= 2;
return a;
}
return bar;
}
var baz = foo();
baz(); // 20
baz(); // 40
baz(); // 80
var blat = foo();
vlat(); // 20
在上述代码中,所返回的对bar函数的引用被赋给变量baz。这个函数现在是在foo外部被调用,但它依然能够访问a。这是因为JavaScript中的作用域是词法性的。函数是运行在定义它们的作用域中(本例中是foo内部的作用域),而不是运行在调用它们的作用域中。只要bar被定义在foo中,它就能访问在foo中定义的所有变量,即使foo的执行已经结束。
这就是闭包的一个例子。在foo返回后,它的作用域被保存下来,但只有它返回的那个函数能够访问这个作用域。在前面的示例中,baz和blat各有这个作用域及a的一个副本,而且只有他们自己能对其进行修改。返回一个内嵌函数是创建闭包最常用的手段。
用闭包实现私有成员
现在回过来看手头的问题:你需要创建一个只能在对象内部访问的变量。
为了创建私有属性,需要在构造器函数的作用域中定义相关变量。这些变量可以被定义于该作用域的所有函数访问,包括哪些特权方法:
1 var Book = function(newIsbn, newTitle, newAuthor){ 2 var isbn, title, author; 3 4 function checkIsbn(isbn){ 5 ... 6 } 7 this.getIsbn = function(){ 8 return isbn; 9 }; 10 this.setIsbn = function(newIsbn){ 11 if(!checkIsbn(newIsbn)){ //此处调用自定义的检查方法 12 throw new Error('Book: Invalid ISBN.'); 13 } 14 isbn = newIsbn; 15 }; 16 this.getTitle = function(){ 17 return title; 18 }; 19 this.setTitle = function(newTitle){ 20 title = newTitle || 'No title specified'; 21 }; 22 this.getAuthor = function(){ 23 return author; 24 } 25 this.setAuthor = function(newAuthor){ 26 author = newAuthor || 'No author specified'; 27 }; 28 29 this.setIsbn(newIsbn); 30 this.setTitle(newTitle); 31 this.setAuthor(newAuthor); 32 }; 33 34 Book.prototype = { 35 display: function(){ 36 ... 37 } 38 };
我们用var声明这些变量,这意味着它们值存在于Book构造器中。checkIsbn函数也是用同样的方式声明的,因此成了一个私有方法。
需要访问这些变量和函数的方法只需声明在Book中即可。这些方法被称为特权方法,因为它们是公有方法,但却能够访问私有属性和方法。为了在对象外部能访问这些特权函数,它们的前面都被加上了关键字this。因为这些方法定义域Book构造器的作用域中,所以它们能访问到私有属性。引用这些属性时并没有使用this关键字,因为它们不是公开的。所有取值器和赋值器方法都被改为不加this地直接引用这些属性。
任何不需要直接访问私有属性的方法都可以在Book.prototype中声明。display就是这类方法中的一个。它不需要直接访问任何私有属性,因为它可以通过调用getIsbn或getTitle来进行间接访问。只有那些需要直接访问私有成员的方法才应该被设计为特权方法。但特权方法太多又会占用过多内存,因为每个对象实例都包含了所有特权方法的新副本。
用这种方式创建的对象可以具有真正私有的属性。其他程序员不可能直接访问他们创建的Book实例的任何内部数据。由于他们不得不通过赋值器方法设置属性的值,所以属性会得到什么样的值尽在你的掌控之中。
弊端:
(1)在门户大开型对象创建模式中,所有方法都创建在原型对象中,因此不管生成多少对象实例,这些方法在内存中只存在一份。而采用上述方法,没生成一个新的对象实例,都将为每一个私有方法和特权方法生成一个新的副本。这会比其他做法耗费更多内存,所以只宜用在真正需要私有成员的场合。
(2)这种对象创建模式也不利于派生子类,因为所派生出的子类不能访问超类的任何私有属性或方法。相比之下,在大多数语言中,子类都能访问超类的所有私有属性和方法。故在JavaScript中用闭包实现私有成员导致的派生问题被称为“继承破坏封装”