JavaScript基础对象创建模式之私有属性和方法(024)
2014-09-17 00:15 Bryran 阅读(463) 评论(0) 编辑 收藏 举报JavaScript没有特殊的语法来表示对象的私有属性和方法,默认的情况下,所有的属性和方法都是公有的。如下面用字面声明的对象:
var myobj = { myprop: 1, getProp: function () { return this.myprop; } }; console.log(myobj.myprop); // `myprop` is publicly accessible console.log(myobj.getProp()); // getProp() is public too
又如下面用构造函数声明的对象:
function Gadget() { this.name = 'iPod'; this.stretch = function () { return 'iPad'; }; } var toy = new Gadget(); console.log(toy.name); // `name` is public console.log(toy.stretch()); // stretch() is public
1. 私有成员
虽 然JavaScript没有提供私有成员的语法表示,但通过局部代码段(closure)提供局部作用域来实现私有的成员。而JavaScript中唯一 提供局部作用域的方式是函数。比如,在构造函数内部使用var声明的变量,就是私有的变量。然后,我们可以通过公有方法来访问这些变量:效果上和Java 中的访问控制是一样的:
function Gadget() { // private member var name = 'iPod'; // public function this.getName = function () { return name; }; } var toy = new Gadget(); // `name` is undefined, it's private console.log(toy.name); // undefined // public method has access to `name` console.log(toy.getName()); // "iPod"
上面的代码展示了JavaScript中实现私有成员的方法,这些私有的属性对于构造函数,以及对构造的对象来说是局部的(私有的),但对外不可见。
2. 特权方法
所谓的特权方法其实也没有特别的语法表示,而是特指可以向外提供私有变量访问的方法而已。比如上面代码里的 getName() 方法就是一个特权方法,它提供对私有属性name的访问控制。
3. 对私有成员访问控制的失效
私有成员的访问控制也可能会失效,常见的情况有如下:
- 早期版本的Firefox向eval()提供了第二个参数,使得函数中的私有成员可以被访问;
- Mozilla Rhino提供__parent__属性,访问函数的私有成员;
- 当特权方法直接返回私有属性的引用,而这个私有属性恰好又是一个对象(数组也是对象)时;外部代码就可以通过对象引用修改对象的内容。
上面的两种情况在现在的主流浏览器中都不能实现了,最需要防范的是第三种情况,如下面的代码:
function Gadget() { // private member var specs = { screen_width: 320, screen_height: 480, color: "white" }; // public function this.getSpecs = function () { return specs; }; }
getSpecs() 返回了specs对象的引用,这时外部代码就不仅得到了specs对象中存储的数值,还可以随时修改其中的数值:
var toy = new Gadget(), specs = toy.getSpecs(); specs.color = "black"; specs.price = "free"; console.dir(toy.getSpecs());
可 以想象,上面的console.dir返回的结果是什么。要防止对象引用导致的私有成员访问控制的失效,就别把私有成员的引用直接返回。解决办法可以是返 回一个克隆的对象,或是只返回私有成员中需要返回的简单数据类型的数值。后者又被称为 Principle of Least Authority (POLA)。比如在上面的例子中,如果代码的使用者调用getSpecs()的目的是得到Gadget的尺寸,那么就可以提供一个 getDimensions()方法,返回一个新建的(new出来的)存有width和height的对象。如果非要返回specs对象,也可以通过另外 实现的克隆方法返回一个specs的拷贝。许多JavaScript库都提供如 extend()或 extendDeep()等API,实现了对象的浅 拷贝和深拷贝。
4. 字面声明的对象中的私有成员
上面介绍了使用构造函数时对象的私有成员,而使用字面声明法创建的对象也同样可以实现私有成员,做法在原理上相同:使用函数的局部作用域,提供局部的(私有的)变量。这时我们就要用到一个即时函数了:
var myobj; // this will be the object (function () { // private members var name = "my, oh my"; // implement the public part // note -- no `var` myobj = { // privileged method getName: function () { return name; } }; }()); myobj.getName(); // "my, oh my"
如果用即时函数返回这个新建的对象,效果也相同:
var myobj = (function () { // private members var name = "my, oh my"; // implement the public part return { getName: function () { return name; } }; }()); myobj.getName(); // "my, oh my"
这种写法其实也是模块模式的最初实现,这里先提一下,后面再详细介绍。
5. Prototype和私有成员
使 用局部变量或方法来声明私有成员的一个不太完美的地方是,这些局部属性和方法在每次创建对象时都将创建一次。这一问题也同样出现在构造函数中使用this 为对象添加成员属性时。解决方法是把常用的成员(包括公有的和私有的)放到protoytpe里去声明,这样成员属性和方法就只创建一次了。下面的实现用 到两种模式:在构造函数中实现私有属性的模式和在字面声明的对象中实现私有属性。在字面声明方式声明的prototype中创建的私有属性将分配给每个将 创建的对象,而且只prototype只会初始化一次。
function Gadget() { // private member var name = 'iPod'; // public function this.getName = function () { return name; }; } Gadget.prototype = (function () { // private member var browser = "Mobile Webkit"; // public prototype members return { getBrowser: function () { return browser; } }; }()); var toy = new Gadget(); console.log(toy.getName()); // privileged "own" method console.log(toy.getBrowser()); // privileged prototype method
6. 揭露模式(Revelation Pattern):把私有方法变成公有
我们可以在任何实现私有方法的模式之上,把私有方法使用公有接口揭露出来。这种模式可以在保护私有方法的同时,向外提供私有方法的访问入口。下面是一种实现的方式:
var myarray; (function () { var astr = "[object Array]", toString = Object.prototype.toString; function isArray(a) { return toString.call(a) === astr; } function indexOf(haystack, needle) { var i = 0, max = haystack.length; for (; i < max; i += 1) { if (haystack[i] === needle) { return i; } } return ?1; } myarray = { isArray: isArray, indexOf: indexOf, inArray: indexOf }; }());
上面的代码为使用字面声明法创建的对象的私有方法提供公有接口。类似的,也可以在使用构造函数创建对象的时候实现这一模式。在上面的代码 里,isArray()和indexOf()都是私有方法,而字面声明中为indexOf()提供了两个公有接口:JavaScript风格的 indexOf()和PHP风格的inArray()。这两个公有的接口的使用方法完全相同:
myarray.isArray([1,2]); // true myarray.isArray({0: 1}); // false myarray.indexOf(["a", "b", "z"], "z"); // 2 myarray.inArray(["a", "b", "z"], "z"); // 2
通过这一模式,一但外面程序修改了公有接口,内部的私有实现将不受影响。比如修改公有的indexOf()接口,将它指向null,不会影响inArray()的使用:
myarray.indexOf = null; myarray.inArray(["a", "b", "z"], "z"); // 2