私有属性和方法

JavaScript不像Java或者其它语言,它没有专门的提供私有、保护、公有属性和方法的语法。所有的对象成员都是公有的:

  1. var myobj = {
  2. myprop: 1,
  3. getProp: function () {
  4. return this.myprop;
  5. }
  6. };
  7. console.log(myobj.myprop); // myprop是公有的
  8. console.log(myobj.getProp()); // getProp()也是公有的

当你使用构造函数创建对象的时候也是一样的,所有的成员都是公有的:

  1. function Gadget() {
  2. this.name = 'iPod';
  3. this.stretch = function () {
  4. return 'iPad';
  5. };
  6. }
  7. var toy = new Gadget();
  8. console.log(toy.name); // name是公有的
  9. console.log(toy.stretch()); // stretch()也是公有的

私有成员

尽管语言并没有用于私有成员的专门语法,但你可以通过闭包来实现。在构造函数中创建一个闭包,任何在这个闭包中的部分都不会暴露到构造函数之外。但是,这些私有变量却可以被公有方法访问,也就是在构造函数中定义的并且作为返回对象一部分的那些方法。我们来看一个例子,name是一个私有成员,在构造函数之外不能被访问:

  1. function Gadget() {
  2. // 私有成员
  3. var name = 'iPod';
  4. // 公有函数
  5. this.getName = function () {
  6. return name;
  7. };
  8. }
  9. var toy = new Gadget();
  10. // name是是私有的
  11. console.log(toy.name); // undefined
  12. // 公有方法可以访问到name
  13. console.log(toy.getName()); // "iPod"

如你所见,在JavaScript创建私有成员很容易。你需要做的只是将私有成员放在一个函数中,保证它是函数的本地变量,也就是说让它在函数之外不可以被访问。

特权方法

特权方法的概念不涉及到任何语法,它只是一个给可以访问到私有成员的公有方法的名字(就好像它们有更多权限一样)。

在前面的例子中,getName()就是一个特权方法,因为它有访问name属性的特殊权限。

私有成员失效

当你使用私有成员时,需要考虑一些极端情况:

  • 在Firefox的一些早期版本中,允许通过给eval()传递第二个参数的方法来指定上下文对象,从而允许访问函数的私有作用域。比如在Mozilla Rhino(译注:一个JavaScript引擎)中,允许使用__parent__来访问私有作用域。这些极端情况现在并没有广泛存在于浏览器中。
  • 当你直接通过特权方法返回一个私有变量,而这个私有变量恰好是一个对象或者数组时,外部的代码可以修改这个私有变量,因为它是按引用传递的。

我们来看一下第二种情况。下面的Gadget的实现看起来没有问题:

  1. function Gadget() {
  2. // 私有成员
  3. var specs = {
  4. screen_width: 320,
  5. screen_height: 480,
  6. color: "white"
  7. };
  8. // 公有函数
  9. this.getSpecs = function () {
  10. return specs;
  11. };
  12. }

这里的问题是getSpecs()返回了一个specs对象的引用。这使得Gadget()的使用者可以修改貌似隐藏起来的私有成员specs

  1. var toy = new Gadget(),
  2. specs = toy.getSpecs();
  3. specs.color = "black";
  4. specs.price = "free";
  5. console.dir(toy.getSpecs());

在Firebug控制台中打印出来的结果如图5-2:

图5-2 私有对象被修改了

图5-2 私有对象被修改了

这个问题有点出乎意料,解决方法就是不要将你想保持私有的对象或者数组的引用传递出去。达到这个目标的一种方法是让getSpecs()返回一个新对象,这个新对象只包含对象的使用者需要的数据。这也是众所周知的“最低授权原则”(Principle of Least Authority,简称POLA),指永远不要给出比真实需要更多的东西。在这个例子中,如果Gadget()的使用者关注它是否适应一个特定的盒子,它只需要知道尺寸即可。所以你应该创建一个getDimensions(),用它返回一个只包含widthheight的新对象,而不是把什么都给出去。也就是说,也许你根本不需要实现getSpecs()方法。

当你需要传递所有的数据时,有另外一种方法,就是使用通用的对象复制函数创建specs对象的一个副本。下一章提供了两个这样的函数——一个叫extend(),它会浅复制一个给定的对象(只复制顶层的成员),另一个叫extendDeep(),它会做深复制,遍历所有的属性和嵌套的属性。

对象字面量和私有成员

到目前为止,我们只看了使用构建函数创建私有成员的示例。如果使用对象字面量创建对象时会是什么情况呢?是否有可能含有私有成员?

如你前面所看到的那样,私有数据使用一个函数来包裹。所以在使用对象字面量时,你也可以使用一个即时函数创建的闭包。例如:

  1. var myobj; // 一个对象
  2. (function () {
  3. // 私有成员
  4. var name = "my, oh my";
  5. // 实现公有部分,注意没有var
  6. myobj = {
  7. // 特权方法
  8. getName: function () {
  9. return name;
  10. }
  11. };
  12. }());
  13. myobj.getName(); // "my, oh my"

还有一个原理一样但看起来不一样的实现示例:

  1. var myobj = (function () {
  2. // 私有成员
  3. var name = "my, oh my";
  4. // 实现公有部分
  5. return {
  6. getName: function () {
  7. return name;
  8. }
  9. };
  10. }());
  11. myobj.getName(); // "my, oh my"

这个例子也是所谓的“模块模式”的基础,我们稍后将讲到它。

原型和私有成员

使用构造函数创建私有成员的一个弊端是,每一次调用构造函数创建对象时这些私有成员都会被创建一次。

这对在构建函数中添加到this的成员来说是一个问题。为了避免重复劳动,节省内存,你可以将共用的属性和方法添加到构造函数的prototype(原型)属性中。这样的话这些公共的部分会在使用同一个构造函数创建的所有实例中共享。你也同样可以在这些实例中共享私有成员,甚至可以将两种模式联合起来达到这个目的,同时使用构造函数中的私有属性和对象字面量中的私有属性。因为prototype属性也只是一个对象,可以使用对象字面量创建。

这是一个示例:

  1. function Gadget() {
  2. // 私有成员
  3. var name = 'iPod';
  4. // 公有函数
  5. this.getName = function () {
  6. return name;
  7. };
  8. }
  9. Gadget.prototype = (function () {
  10. // 私有成员
  11. var browser = "Mobile Webkit";
  12. // 公有函数
  13. return {
  14. getBrowser: function () {
  15. return browser;
  16. }
  17. };
  18. }());
  19. var toy = new Gadget();
  20. console.log(toy.getName()); // 自有的特权方法
  21. console.log(toy.getBrowser()); // 来自原型的特权方法

将私有函数暴露为公有方法

“暴露模式”是指将已经有的私有函数暴露为公有方法,它在你希望尽量保护对象内的一些方法不被外部修改干扰的时候很有用。你希望能提供一些功能给外部访问,因为它们会被用到,如果你把这些方法公开,就会使得它们不再健壮,因为你的API的使用者可能修改它们。在ECMAScript5中,你可以选择冻结一个对象,但在之前的版本中这种方法不可用。下面进入暴露模式(原来是由Christian Heilmann创造的模式,叫“暴露模块模式”)。

我们来看一个例子,它建立在对象字面量的私有成员模式之上:

  1. var myarray;
  2. (function () {
  3. var astr = "[object Array]",
  4. toString = Object.prototype.toString;
  5. function isArray(a) {
  6. return toString.call(a) === astr;
  7. }
  8. function indexOf(haystack, needle) {
  9. var i = 0,
  10. max = haystack.length;
  11. for (; i < max; i += 1) {
  12. if (haystack[i] === needle) {
  13. return i;
  14. }
  15. }
  16. return 1;
  17. }
  18. myarray = {
  19. isArray: isArray,
  20. indexOf: indexOf,
  21. inArray: indexOf
  22. };
  23. }());

这里有两个私有变量(私有函数)——isArray()indexOf()。在包裹函数的最后,用那些允许被从外部访问的函数填充myarray对象。在这个例子中,同一个私有函数 indexOf()同时被暴露为ECMAScript5风格的indexOf()和PHP风格的inArry()。测试一下myarray对象:

  1. myarray.isArray([1,2]); // true
  2. myarray.isArray({0: 1}); // false
  3. myarray.indexOf(["a", "b", "z"], "z"); // 2
  4. myarray.inArray(["a", "b", "z"], "z"); // 2

现在假如有一些意外的情况发生在暴露的indexOf()方法上,私有的indexOf()方法仍然是安全的,因此inArray()仍然可以正常工作:

  1. myarray.indexOf = null;
  2. myarray.inArray(["a", "b", "z"], "z"); // 2