这一篇,我们主要来学习一下私有属性和方法以及模块模式。
三、私有属性和方法
JavaScript并没有特殊的语法来表示私有、保护、或公共属性和方法,在这一点上与Java或其他语言是不同的。JavaScript中所有对象的成员是公共的:
var myobj = { myprop:1, getProp: function() { return this.myprop; } }; console.log(myobj.myprop); // 'myprop'是共有可访问的 console.log(myobj.getProp()); //getProp()也是公有可访问的 // 当使用构造函数创建对象时也通用如此,即所有的成员仍然都是公共的: function Gadget() { this.name = 'iPod'; this.stretch = function () { return 'iPad'; }; } var toy = new Gadget(); console.log(toy.name); // 'name'是共有的 console.log(toy.stretch()); //stretch()是公有的
私有成员
虽然JavaScript语言中并没有用于私有成员的特殊语法,但是可以使用闭包来实现这种功能。构造函数创建了一个闭包,而在闭包范围内部的任意变量都不会暴露给构造函数以外的代码。然而,这些私有变量仍然可以用于公共方法中:即定义在构造函数中,且作为返回对象的一个部分暴露给外部的方法。
我们来看个例子,其中name是一个私有成员,在构造函数外部是不可访问的:
function Gadge() { // 私有成员 var name = 'iPod'; // 公有函数 this.getName = function () { return name; }; } var toy = new Gadge(); // 'name'是undefined的,它是私有的 console.log(toy.name);//undefined // 公有方法访问'name' console.log(toy.getName());// 'iPod'
正如所看到的,很容易在JavaScript中实现私有性。需要做的只是在函数中将需要保持为私有属性的数据包装起来,并确保它对函数来说是局部变量,这意味着外部函数不能访问它。
特权方法
特权方法(Privileged Method)的概念并不涉及任何特殊语法,它只是指那些可以访问私有成员的公共方法(因此它拥有更多的特权)的一个名称而已。
在前面的例子中,getName()就是一个特权方法,它具有访问私有属性name的“特殊”权限。
私有性失效
当关注私有的时候就会出现一些边缘情况:
- 旧版本浏览器的一些情况比如Firefox的eval()可以传递第二个上下文参数,比如Mozilla的__parent__属性也与此类似。但是这几乎都是在古代浏览器才存在,现代浏览器几乎已经不存在这种情况了。
- 当直接从一个特权方法中返回一个私有变量,且该变量恰好是一个对象或者数组,那么外面的代码仍然可以访问该私有变量,这是因为它是通过引用传递的。
我们来看下这种情况。以下Gadget的实现看起来就像是无意造成失效的:
function Gadget() { // 私有成员 var specs = { screen_width:320, screen_height:480, color:"white" }; // 公有函数 this.getSpecs = function () { return specs; }; } // 这里的问题是在于getSpecs()方法返回了一个引用的specs对象。这使得Gadget的用户可以修改表面上看起来是隐藏和私有的specs对象: var toy = new Gadget(), specs = toy.getSpecs(); specs.color = "black"; specs.price = "free"; console.dir(toy.getSpecs());
对于这种意外行为的解决方法是保持细心,既不要传递需要保持私有性的对象和数组的引用。解决这个问题的一种方法是,使getSpecs()返回一个新对象,而该对象仅包含客户关注的原对象中的数据。这也是众所周知的最低授权原则(Principle of Least Authority,POLA),其中规定了应该永远不要给予超过需要的特权。
在这种情况下,如果Gadget的消费者仅关注该gadget组建是否与一个特定方框的尺寸相符合,那么它需要的仅是尺寸规格。因此,并不需要分发所有的数据,可以创建getDimensions()使其返回一个包含宽度和高度的新对象。此时,可能根本不需要实现getSpecs()。
当需要传递所有的数据时,另外一种解决方法是使用一个通用性的对象克隆(object cloning)函数以创建specs对象副本。下一章提供了两个这样的函数,其中一个名为extend(),它可以针对给定对象创建一个浅复制(shallow copy)副本(仅复制顶级单数)。而另一个名为extendDeep()的函数,它可以通过递归复制所有的属性以及其嵌套属性而创建深度复制(deep copy)副本。
对象字面量以及私有性
到目前为止,我们仅看到了使用构造函数获得私有性的例子。但是当使用对象字面量(object literal)来创建对象会是什么情况?他是否还有可能拥有私有成员?
正如在前面所看到的,需要的只是一个能够包装私有数据的函数。因此,在使用对象字面量的情况下,可以使用一个额外的匿名即时函数(anonymous immediate function)创建闭包来实现私有性:
var myobj; //这将会是对象 (function() { // 私有成员 var name = 'my,oh my'; // 实现公有部分 // 注意,没有'var'修饰符 myobj = { // 特权方法 getName: function () { return name; } }; }()); myobj.getName(); //'my, oh my'; // 下面的例子与上面的具有同样的思想,但是在实现上略有不同: var myobj = (function () { // 私有成员 var name = 'my,oh my'; // 实现公有部分 return { getName:function () { return name; } }; }()); // 这个例子也是模块模式的基础框架,后面会再聊。
原型和私有性
当将私有成员与构造函数一起使用时,其中有一个缺点在于每次调用构造函数以创建对象时,这些私有成员都会被重新创建。构造函数中添加到this中的任何成员实际上都面临以上问题。为了避免复制工作以及节省内存,可以将重用属性和方法添加到构造函数的prototype属性中。这样,通过同一个构造函数创建的多个实例可以共享常见的部分数据。此外,还可以再多个实例中共享隐藏的私有成员。为了实现这一点,可以使用以下两个模式的组合:即构造函数中的私有属性以及对象字面了中的私有属性。由于prototype属性仅是一个对象,因此可以使用对象字面了创建该对象。
function Gadget() { // 私有成员 var name = 'iPod'; // 公有函数 this.getName = function () { return name; }; } Gadget.prototype = (function () { // 私有成员 var browser = "Mobile Webkit"; // 公有原型成员 return { getBrowser: function() { return browser; } }; }()); var toy = new Gadget(); console.log(toy.getName()); //自身特权方法 console.log(toy.getBrowser());// 原型特权方法
将私有方法揭示为公共方法
揭示模式(revelation pattern)可用于将私有方法暴露成为公共方法。当为了对象的运转而将所有功能都放置在一个对象中,以及,想尽可能的保护该对象的时候,这种揭示模式就显得非常有用。不过,同时可能也想为其中的一些功能提供公共可访问的接口,因为那可能也是有用的。当这些私有方法暴露为公共方法时,也使他们变得更为脆弱。因为使用公共API的一些用户可能会修改原对象,甚至是无意的修改。在ES5中,可以选择将一个对象冻结,但是在前一版本的语言中是不具备该功能的。
揭示模式的前提,是建立在对象字面量的私有成员之下的。
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()。 // 在匿名函数(immediate function)的最后,对象myarray中填充了认为适用于公共访问的功能。 // 在这种情况下,同一个私有函数indexOf()可以暴露为ES5风格的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()方法发生意外,但私有indexOf()方法仍然是安全的,因此inArray()将继续正常运行: myarray.indexOf = null; myarray.inArray(["a","b","z"],"z"); // 2
四、模块模式
目前模块模式得到了广泛的应用,因为它提供了结构化的思想并且有助于组织日益增长的代码。与其他语言不同的是,JavaScript并没有(package)的特殊语法,但是模块模式提供了一种创建自包含非耦合(self-contained de-coupled)代码片段的有利工具,可以将它视为黑盒功能,并且可以根据您所编写软件的需求添加、替换或删除这些模块。
模块模式是本系列中迄今为止介绍过的第一种多种模式组合的模式,也就是以下模式的组合:命名空间、即时函数、私有和特权成员、声明依赖。
该模式的第一步时间里一个命名空间。让我们使用本章前面介绍的namespace()函数,并且启动可以提供有用数组方法的工具模块。
MYAPP.namespace('MYAPP.utilities.array'); // 下一步是定义该模块。对于需要保持私有性的情况,本模式使用了一个可以提供私有作用域的即时函数。 // 该即时函数返回了一个对象,即具有公共接口的实际模块,可以通过这些接口来使用这些模块。 MYAPP.utilities.array = (function () { return { // todo... } }()); // 接下来我们向该公共接口添加一些方法: MYAPP.utilities.array = (function () { return { inArray:function(needle,haystack) { // ... }, isArray:function(a) { // ... } }; }());
通过使用由即时函数提供的私有作用域,可以根据需要声明一些私有属性和方法。在即时函数的顶部,正好也就是声明模块可能由任何依赖的为止。在变量声明之后,可以任意地放置有助于建立该模块的任何一次性的初始化代码。最终结果是一个由即时函数返回的对象,其中该对象包含了您模块的公共API:
MYAPP.namespace('MYAPP.utilities.array'); // 接下来我们向该公共接口添加一些方法: MYAPP.utilities.array = (function () { // 依赖 var uobj = MYAPP.utilities.object, ulang = MYAPP.utilities.lang, // 私有属性 array_string = "[Object Array]", ops = Object.prototype.toString; // 私有方法 // ... // var 变量定义结束 // 可选的一次性初始化过程 // ... return { inArray:function(needle,haystack) { for(var i = 0;i < haystack.length; i += 1) { if(haystack[i] === needle) { return true; } } }, isArray:function(a) { return ops.call(a) === array_string; } // 更多方法和属性 }; }());
模块模式得到了广泛的使用,并且强烈建议使用这种方式组织您的代码,尤其是当旧代码日益增长的时候。
揭示模块模式
我们已经讨论了揭示模式,同时还考虑了私有模式。模块模式也可以组织成与之相似的方式,其中所有的方法都需要保持私有性,并且只能暴露那些最后决定设立API的那些方法。根据这些思想,代码是这样的:
MYAPP.namespace('MYAPP.utilities.array'); MYAPP.utilities.array = (function () { // 私有属性 var array_string = "[Object Array]", ops = Object.prototype.toString, // 私有方法 inArray = function (needle,haystack) { for(var i = 0;i < haystack.length; i += 1) { if(haystack[i] === needle) { return true; } } return -1; }, isArray = function(a) { return ops.call(a) === array_string; } // var 变量定义结束 // 揭示公有API return { isArray:isArray, indexOf:inArray }; }());
创建构造函数的模块
前面的例子中创建了一个对象MYAPP.utilities.array,但有时候使用构造函数创建对象更为方便。当然,可以仍然使用模块模式来执行创建对象的操作。它们之间唯一的区别在于包装了模块的即时函数最终将会返回一个函数,而不是返回一个对象。
考虑以下使用模块模式的例子,在该例子中创建了一个构造函数MYAPP.utilities.Array:
MYAPP.namespace('MYAPP.utilities.Array'); MYAPP.utilities.Array = (function () { // 依赖 var uobj = MYAPP.utilities.object, ulang = MYAPP.utilities.lang, // 私有属性和方法 Constr; // var 变量定义结束 // 可选的一次性初始化过程 // ... // 公有API——构造函数 Constr = function(o) { this.elements = this.toArray(o); }; // 公有API——原型 Constr.prototype = { constructor:MYAPP.utilities.Array, version:"2.0", toArray:function(obj) { for(var i = 0,a = [],len = obj.length; i < len; i += 1) { a[i] = obj[i] } return a; } }; // 返回要分配给新命名空间的构造函数 return Constr; }()); // 这样使用 var arr = new MYAPP.utilities.Array(obj);
将全局变量导入到模块中
在常见的变化模式中,可以将参数传递到包装了模块的即时函数中。可以传递任何值,但是通常这些都是对全局变量、甚至是全局对象本身的引用。导入全局变量有助于加速即时函数中的全局符号解析的速度,因为这些导入的变量成为了该函数的局部变量。
MYAPP.utilities.module = (function(app,global) { // 引用全局对象 // 以及现在被转换成局部变量的全局应用程序命名空间对象 }(MYAPP,this));
好了,这一篇就到这里了,上诉的代码,实用价值是很大的。希望大家可以仔细阅读,认真看看。嘿嘿。