转载:
对象是JavaScript的基本单位。实际上JavaScript中一切都是对象并得益于这一事实。然而,为了增强这一纯粹的面向对象的语言,JavaScript包括了一个庞大的功能集,使它无论是在潜在能力还是风格上,都成为一门极其独特的语言。
本章中我将开始覆盖JavaScript语言的最重要的一些方面,如引用,作用域,闭包,以及上下文,你会发现这正是其它JavaScript书籍中很少论及的。打下主要的基础以后,我们将开始探索面向对象JavaScript的几个重点,包括对象到底如何运作和怎样创建新的对象并在特定的许可条件下设置其方法。如果你认真去读的话,这很可能是本书中最重要的一章,它将彻底地改变你看待JavaScript作为一门编程语言的方式。
语言特性
引用
JavaScript的一个重要的方面是引用的概念。引用就是指向对象实际位置的指针。这是一项极其强大的功能。前提是,实际的对象决不是一个引用:字符串总是一个字符串,数组总是一个数组。然而,多个变量可以引用相同的对象。JavaScript就是以这种引用引用机制为基础。通过维护一系列的指向其它对象的引用,语言为你提供了更大的弹性。
另外,对象能包括一系列的属性,这些属性简单地引用其它对象(如字符串,数字,数组等等)。当几个变量指向相同对象时,修改底层对象类型将会在所有的指点向它的变量上有所反映。例2-1即此一例,两个变量指向同一个对象,但是对对象内容的修改的反映是全局的。
程序2-1. 多变量引用单个对象的示例
复制内容到剪贴板
我从前提到过自更改的对象在JavaScript里非常少见的。让我们看一个发生这一状况的实例。数组对象能够用push方法给它自己增加额外的项。因为在数组对象的核心,值是作为对象的属性存储的,结果类似程序2-1中的情形,一个对象成为全局被改动的(导致了多个变量的值被同时改变)。见程序2-2.代码:
//设置obj为一个空对象
var obj = new Object();
//objRef现在引用了别的对象
var objRef = obj;
//修改原始对象的属性
obj.oneProperty = true;
//我们可以发现该变化在两个变量中都可以看到
//(因为他们引用了同一个对象)
alert( obj.oneProperty === objRef.oneProperty );
程序2-2. 自修改对象的例子
复制内容到剪贴板
记住这一点是很重要的:引用总是只指向最终被引用的对象,而不会是引用本身。例如,在Perl语言里,很可能有一个引用指向另一个也是引用的变量。但在JavaScript里,它会沿着引用链向下追溯直到指向核心的对象。程序2-3演示了这种情形,物理的目标已经改变而引用仍然指向原来的对象。代码:
//创建一组项目的数组
var items = new Array( "one", "two", "three" );
//创建一个对项目数组的引用
var itemsRef = items;
//给原始数组添加一项
items.push( "four" );
//两个数组的长度应该相同,
//因为它们都指向相同的数组对象
alert( items.length == itemsRef.length );
程序2-3. Changing the Reference of an Object While Maintaining Integrity(见#9 oerrite 的回复)
复制内容到剪贴板
最后,让我们来看一个陌生的例子,表面似乎是一个自修改的对象,却作用于一个新的未被引用的对象。当执行字符串串联时,结果总是一个新的字符串对象,而非原字符串更改后的版本。这在程序2-4中可以看出。代码:
// 设置items为一个字符串的数组(对象)
var items = new Array( "one", "two", "three" );
// 设置itemsRef为对items的引用
var itemsRef = items;
//让items指向一个新的对象
items = new Array( "new", "array" );
// items和itemsRef现在指向不同的对象
// items指向new Array( "new", "array" )
// itemsRef则指向new Array( "one", "two", "three" )
alert( items !== itemsRef );
程序2-4. 对象修改作用于一个新的对象而非自修改对象的示例
复制内容到剪贴板
如果你刚刚接触,引用可能是个令人头大的***钻话题。然而,理解引用是如何工作的对于编写良好、干净的JavaScript代码是极其重要的。接下来的几节我们将探究几种未必新鲜和令人激动的,但是同样对编写良好、干净的代码很重要的特性。代码:
//让item等于一个新的字符串对象
var item = "test";
//itemRef也引用相同的字符串对象
var itemRef = item;
//在字符串对象上串联一个新的对象
//注意:这创建了一个新的对象,并不修改初始对象
item += "ing";
//item和itemRef的值并不相等,因为
//一个全新的对象被创建了
alert( item != itemRef );
函数重载和类型检查
其它面向对象的语言(比如Java)的一种共有的特性是“重载”函数的能力:传给它们不同数目或类型的参数,函数将执行不同操作。虽然这种能力在JavaScript中不是直接可用的,一些工具的提供使得这种探求完全成为可能。
在JavaScript的每一个函数里存在一个上下文相关的名为arguments的变量,它的行为类似于一个伪数组,包含了传给函数的所有参数。参数不是一真正的数组(意味着你不能修改它,或者调用push()方法增加新的项),但是你可以以数组的形式访问它,而且它也的确有一个length属性。程序2-5中有两个示例。
程序2-5. JavaScript中函数重载的两个示例
复制内容到剪贴板
另外,存在另一种断定传递给一个函数的参数数目的方法。这种特殊的方法多用了一点点技巧:我们利用了传递过来的任何参数值不可能为undefined这一事实。程序2-6展示一了个简单的函数用来显示一条错误消息,如果没有传给它,则提供一条缺省消息。代码:
//一个简单的用来发送消息的函数
function sendMessage( msg, obj ) {
//如果同时提供了一个消息和一个对象
if ( arguments.length == 2 )
//就将消息发给该对象
obj.handleMsg( msg );
//否则,刚假定只有消息被提供
else
//于是显示该消息
alert( msg );
}
//调用函数,带一个参数 – 用警告框显示消息
sendMessage( "Hello, World!" );
//或者,我们也可以传入我们自己的对象用
//一种不同方式来显示信息
sendMessage( "How are you?", {
handleMsg: function( msg ) {
alert( "This is a custom message: " + msg );
}
});
//一个使用任意数目参数创建一个数组的函数
function makeArray() {
//临时数组
var arr = [];
//遍历提交的每一个参数
for ( var i = 0; i < arguments.length; i++ ) {
arr.push( arguments[i] );
}
//返回结果数组
return arr;
}
程序2-6: 显示错误消息和缺省消息
复制内容到剪贴板
typeof语句的使用引入了类型检查。因为JavaScript(目前)是一种动态类型语言,使得这个话题格外有用而重要的话题。有许多种方法检查变量的类型;我们将探究两种特别有用的。代码:
function displayError( msg ) {
//检查确保msg不是undefined
if ( typeof msg == 'undefined' ) {
//如果是,则设置缺省消息
msg = "An error occurred.";
}
//显示消息
alert( msg );
}
第一种检查对象类型的方式是使用显式的typeof操作符。这种有用的方法给我们一个字符串名称,代表变量内容的类型。这将是一种完美的方案,除非变量的类型或者数组或自定义的对象如user(这时它总返回"ojbect",导致各种对象难以区分)。
这种方法的示例见程序2-7
程序2-7. 使用typeof决定对象类型的示例
复制内容到剪贴板
检查对象类型的第二种方式是参考所有JavaScript对象所共有的一个称为constructor的属性。该属性是对一个最初用来构造此对象的函数的引用。该方法的示例见程序2-8。代码:
//检查我们的数字是否其实是一个字符串
if ( typeof num == "string" )
//如果是,则将它解析成数字
num = parseInt( num );
//检查我们的数组是否其实是一个字符串
if ( typeof arr == "string" )
//如果是,则用逗号分割该字符串,构造出一个数组
arr = arr.split(",");
程序2-8. 使用constructor属性决定对象类型的示例
复制内容到剪贴板
表2-1显示了对不同类型对象分别使用我所介绍的两种方法进行类型检查的结果。表格的第一列显示了我们试图找到其类型的对象。每二列是运行typeof Variable(Variable为第一列所示的值)。此列中的所有结果都是字符串。最后,第三列显示了对第一列包含的对象运行Variable.constructor所得的结果。些列中的所有结果都是对象。代码:
//检查我们的数字是否其实是一个字符串
if ( num.constructor == String )
//如果是,则将它解析成数字
num = parseInt( num );
//检查我们的字符串是否其实是一个数组
if ( str.constructor == Array )
//如果是,则用逗号连接该数组,得到一个字符串
str = str.join(',');
表2-1. 变量类型检查
———————————————————————————————
Variable typeof Variable Variable.constructor
———————————————————————————————
{an:"object"} object Object
["an","array"] object Array
function(){} function Function
"a string" string String
55 number Number
true boolean Boolean
new User() object User
——————————————————————————————————
使用表2-1的信息你现在可以创建一个通用的函数用来在函数内进行类型检查。可能到现在已经明显,使用一个变量的constructor作为对象类型的引用可能是最简单的类型检查方式。当你想要确定精确吻合的参数数目的类型传进了你的函数时,严格的类型检查在这种可能会大有帮助。在程序2-9中我们可以看到实际中的一例。
程序2-9. 一个可用来严格维护全部传入函数的参数的函数
复制内容到剪贴板
变量类型检查和参数长度校验本身是很简单的概念,但是可用来实现复杂的方法,给开发者和你的代码的使用者提供更好的体验。接下来,我们将探讨JavaScript中的作用域以及怎么更好的控制它。代码:
//依据参数列表来严格地检查一个变量列表的类型
function strict( types, args ) {
//确保参数的数目和类型核匹配
if ( types.length != args.length ) {
//如果长度不匹配,则抛出异常
throw "Invalid number of arguments. Expected " + types.length +
", received " + args.length + " instead.";
}
//遍历每一个参数,检查基类型
for ( var i = 0; i < args.length; i++ ) {
//如JavaScript某一项类型不匹配,则抛出异常
if ( args[i].constructor != types[i] ) {
throw "Invalid argument type. Expected " +
types[i].name +", received " +
args[i].constructor.name + " instead.";
}
}
}
//用来打印出用户列表的一个简单函数
function userList( prefix, num, users ) {
//确保prefix是一个字符串,num是一个数字,
//且user是一个数组
strict( [ String, Number, Array ], arguments );
//循环处理num个用户
for ( var i = 0; i < num; i++ ) {
//显示一个用户的信息
print( prefix + ": " + users[i] );
作用域
作用域是JavaScript中一个较难处理的特性。所有面向对象的编程语言都有某种形式的作用域;这要看是什么上下文约束着作用域。在JavaScript里,作用域由函数约束,而不由块约束(如while,if,和for里的语句体)。最终可能使得一些代码的运行结果表面上显得怪异(如果你来自一种块作用域语言的话)。程序2-10的例子说明了“函数作用域代码”的含义。
代码2-10. JavaScript中变量作用域是怎样工作的例子
复制内容到剪贴板
在程序2-10中你会发现,变量位于在全局作用域。基于浏览器的JavaScript有趣的一面是,所有的全局变量实际上都是window对象的属性。尽管一些老版本的Opera浏览器或Safari浏览器不是这样,假定浏览器这样工作通常是一个很好的经验规则。程序2-11展示了一个这种例子。代码:
//设置一个等于"test"的全局变量foo
var foo = "test";
//在if块中
if ( true ) {
//设置foo为"new test"
//注意:这仍然是在全局作用域中
var foo = "new test";
}
//正如我们在此处可见,foo现在等于"new test"
alert( foo == "new test" );
//创建一个修改变量foo的函数
function test() {
var foo = "old test";
}
//调用时,foo却驻留在是在函数的作用域里面
test();
//确认一下,foo的值仍然是"new test"
alert( foo == "new test" );
程序2-11. JavaScript的全局变量与window对象的例子
复制内容到剪贴板
最后,让我们来看看当一个变量漏定义时会怎样。程序2-12里,变量foo在test()的作用域里被赋值。但是,程序2-12里实际并没有(用var foo)定义变量的作用域。当变量foo没有明确定义时,它将成为全局变量,即使它只在函数的上下文使用。代码:
//全局变量,包含字符串"test"
var test = "test";
//你会发现,我们的全局变量和window的test属性是相同的
alert( window.test == test );
程序2-12. 隐式全局变量声明的示例
复制内容到剪贴板
到目前应该很明显,尽管JavaScript的作用域不如块作用域语言的严格,它还是相当强大和有特色的。尤其是与下节中叙述的闭包的概念结合起来时,JavaScript语言的强大将展露无遗。代码:
//一个为变量foo赋值的函数
function test() {
foo = "test";
}
//调用函数为foo赋值
test();
//我们发现foo现在是全局变量了
alert( window.foo == "test" );
闭包
闭包意味着内层的函数可以引用存在于包绕它的函数的变量,即使外层的函数的执行已经终止。这一特殊的论题可能是非常强大又非常复杂的。我强烈推荐你们参考本节后面将提及的站点,因为它有一些关于闭包这一话题的精彩的信息。
我们先来看程序2-13所示的闭包的两个简单例子。
程序2-13. 闭包改善的代码清晰性的两例
复制内容到剪贴板
第一个对setTimeout的函数调用,展示了一个的JavaScript新手遇到问题的通俗的例子。在JavaScript新手的程序里像这样的代码时常可以看到:
代码:
//得到id为"main"的元素
var obj = document.getElementById("main");
//改变它的边框样式
obj.style.border = "1px solid red";
//初始化一个1秒钟以后被调用的回调函数
setTimeout(function(){
//此函数将隐藏该元素
obj.style.display = 'none';
}, 1000);
//用来延迟显示消息的通用函数
function delayedAlert( msg, time ) {
//初始化一个被封套的函数
setTimeout(function(){
//此函数使用了来自封套它的函数的变量msg
alert( msg );
}, time );
}
//调用函数delayedAlert,带两个参数
delayedAlert( "Welcome!", 2000 );
复制内容到剪贴板
使用闭包的概念,完全可能的把这种混乱的代码清理掉。第一个例子很简单;有一个回调函数在调用setTimeout函数以后1000微秒以后被调用,而它仍引用了变量obj(定义在全局范围,指向id为"main"的元素)。定义的第二个函数,delayedAlert,展示了一种解决出现的setTimeout混乱的方案,以及函数作用域内可以有闭包的能力。代码:
setTimeout("otherFunction()", 1000);
//或者甚至
setTimeout("otherFunction(" + num + "," + num2 + ")", 1000);
你们应该可以发现,当在代码中使用这种简单的闭包时,你所写的东西的清晰性将会提高,免于陷入语法的迷雾之中。
我们来看一个闭包可能带来的有有趣的副作用。在某些函数化的编程语言里,有一个叫做currying的概念。本质上讲,currying是就是为函数的一些参数预填入值,创建一个更简单的新函数的方法。代码2-14里有一个简单的currying的例子,创建了向另一个函数预填一个参数而得的新函数。
代码2-14. 使用闭包的函数currying
复制内容到剪贴板
闭包还能解决另一个常见的JavaScript编码方面的问题。JavaScript新手趋向于在全局作用域里放置许多变量。这一般被认为是不好的习惯,因为那些变量可能悄悄地影响其它的库,导致令人迷惑的问题的产生。使用一个自执行的、匿名的函数,你可以从根本上隐藏所有的通常的全局变量,使它们对其它代码不可见,如程序2-15所示。代码:
//生成做加法的新函数的函数
function addGenerator( num ) {
//返回一个简单函数用来计算两个数的加法,
//其中第一个数字从生成器中借用
return function( toAdd ) {
return num + toAdd
};
}
//addFive现在是接受一个参数的函数,
//此函数将给参数加5,返回结果数字
var addFive = addGenerator( 5 );
//这里我们可以看到,当传给它参数4的时候
//函数addFive的结果为9
alert( addFive( 4 ) == 9 );
代码2-15. 使用匿名函数从全局作用域隐藏变量的例子
复制内容到剪贴板
最后,让我们来看使用闭包时出现的一个问题。闭包允许你引用存在于父级函数中的变量。然而,它并不是提供该变量创建时的值;它提供的是父级函数中该变量最后的值。你会看到这个问题最通常是在一个for循环中。有一个变量被用作迭代器(比如i),在for内部新的函数被创建,并使用了闭包来引用该迭代器。问题是,当新的闭包函数被调用时,它们将会引用该iterator最后的值(比如,一个数组的最后位置),而不是你所期望的那个。程序2-16的例子说明,使用匿名函数激发作用域,在其中创建一个合乎期望的闭包是可能的。代码:
//创建一个用作包装的匿名函数
(function(){
//这个变量通常情况下应该是全局的
var msg = "Thanks for visiting!";
//为全局对象绑定新的函数
window.onunload = function(){
//使用了“隐藏”的变量
alert( msg );
};
//关闭匿名函数并执行之
})();
程序2-16. 使用匿名函数激发一个创建多个闭包函数所需的作用域的例子
复制内容到剪贴板
闭包的概念并非轻易可以掌握的;我着实花了大量的时间和精力才彻底弄清闭包有多么强大。幸运的是,有一个精彩的资源解释了JavaScript中的闭包是怎么工作的:Jim Jey的"JavaScript闭包",网址是http://jibbering.com/faq/faq_notes/closures.html。代码:
//id为"main"的一个元素
var obj = document.getElementById("main");
//用来绑定的items数组
var items = [ "click", "keypress" ];
//遍历items中的每一项
for ( var i = 0; i < items.length; i++ ) {
//用自执行的匿名函数来激发作用域
(function(){
//在些作用域内存储值
var item = items[i];
//为obj元素绑定函数
obj[ "on" + item ] = function() {
//item引用一个父级的变量,
//该变量在此for循环的上文中已被成功地scoped(?)
alert( "Thanks for your " + item );
};
})();
}
最后,我们将研究上下文的概念,这是许多JavaScript的面向对象特性赖以建立的基石。
上下文
在JavaScript中,你的代码将总是有着某种形式的上下文(代码在其内部工作的对象)。这也是其它面向对象语言所共有的功能,但它们都不如JavaScript处理得这样极端。
上下文是通过变量this工作。变量this总是引用代码当前所在的那个对象。记住全局对象实际上是window对象的属性。这意味着即使是在全局上下文里,this变量仍然引用一个对象。上下文可以成为一个强大的工具,是面向对象代码不可或缺的一环。程序2-17展示了一些关于上下文的简单例子。
程序2-17. 在上下文中使用函数然后将其上下文切换到另一个变量的例子
复制内容到剪贴板
你可能已经注意到,在程序2-17中,当我们切换obj.no方法的上下文到变量window时,笨重的代码需要切换函数的上下文。幸运的是,JavaScript提供了两种方法使这一过程变得更加易于理解和实现。程序2-18展示了恰能些目的的两种不同方法,call和apply。代码:
var obj = {
yes: function(){
// this == obj
this.val = true;
},
no: function(){
this.val = false;
}
};
//我们看到,obj对象没有"val"的属性
alert( obj.val == null );
//我们运行yes函数,它将改变附着在obj对象的val属性
obj.yes();
alert( obj.val == true );
//然而,我们现在让window.no指向obj.no方法,并运行之
window.no = obj.no;
window.no();
//这导致obj对象保持不变(上下文则切换到了window对象),
alert( obj.val == true );
//而window的val属性被更新
alert( window.val == false );
程序2-18. 改变函数上下文的示例
复制内容到剪贴板
上下文的有用性此处可能还没有立即显现。当我们进入下一节"面向对象的JavaScript"时,它会变得更加明显。代码:
//一个简单的设置其上下文的颜色风格的函数
function changeColor( color ) {
this.style.color = color;
}
//在window对象上调用这个函数将会出错,因为window没有style对象
changeColor( "white" );
//得到一个id为"main"的对象
var main = document.getElementById("main");
//用call方法改变它的颜色为黑
//call方法将第一个参数设置为上下文,
//并其它所有参数传递给函数
changeColor.call( main, "black" );
//一个设置body元素的颜色的函数
function setBodyColor() {
//apply方法设置上下文为body元素
//第一个参数为设置的上下文,
//第二个参数是一个被作为参数传递给函数的数组
// of arguments that gets passed to the function
changeColor.apply( document.body, arguments );
}
//设置body元素的颜色为黑
setBodyColor( "black" );
面向对象基础
"面向对象的JavaScript"这一说法多少有些冗余,因为JavaScript语言本就是完全面向对象的,不可能有另外的用法。但是,初学编程者(包括JavaScript编程者)共有的一个缺点就是,功能性地编写代码而不使用任何上下文或分组。要完全理解怎么编写优化的JavaScript代码,你必须理解JavaScript的对象是怎样工作的,它们与其它语言有怎样的不同,以及怎样让它们为你所用。
本章的剩余部分我们将讨论用JavaScript编写面向对象代码的基础,在后面的几章中,我们将看到以这种方式编写代码的实例。
对象
对象是JavaScript的基础。实际上JavaScript语言中的一切都是对象,JavaScript的多数能力也正起源于此。在其最根本的层面上,对象作为属性的集合存在,差不多类似于你在其它语言中看到的哈希的概念。程序2-19展示了创建两个带有一组属性的对象的基本示例。
程序2-19. 创建简单对象并设置其属性的两个例子
复制内容到剪贴板
实际上对象就这么回事了。然而,事情变得麻烦的地方,在于新对象(尤其是那些继承其它对象属性的对象)的创建。代码:
//创建一个新对象并将其存放在obj里
var obj = new Object();
//将该对象的一些属性设置成不同的值
obj.val = 5;
obj.click = function(){
alert( "hello" );
};
//下面是等效的代码,使用了{...}式缩写,
//和定义对象属性的"名称-值"对
var obj = {
//用名称-值对设置对象属性
val: 5,
click: function(){
alert( "hello" );
}
};
对象创建
不像大多数其它面向对象的语言,JavaScript实际上并没有类的概念。在大多数其它的面向对象语言中,你可以初始化一个特定的类的实例,但是在JavaScript中的情况这是这样。在JavaScript中,对象能够创建新的对象,对象可以从继承自其它对象。整个概念被称为"prototypal inheritance"(原型标本继承),将在"公有方法"一节中有更多论述。
然而,重要的是,不论JavaScript采用哪种对象方案,总归要有一个方式来创建新的对象。JavaScript的做法是,任何一个函数也都能作为一个对象被实例化。实际上,事情听起来远比它本身更令人困惑。好比有一块生面团(相当于原始的对象),用小甜饼切割器(相当于对象构造器,使用对象的原型prototype)为其成形。
让我们看看程序2-20中这一机制的工作的实例
程序2-20. 创建并使用一个简单的对象
复制内容到剪贴板
程序2-20说明了constructor属性的使用。这个存在于每一个对象中的属性将总是指向创建该对象的那个函数。于是,你可以方便的复制该对象,创建一个新的有共同基类和不同属性的对象。示例见程序2-21.代码:
//一个简单的函数,接受一个参数name,
//并将其保存于当前上下文中
function User( name ) {
this.name = name;
}
//用指定的name创建上述函数的新实例
var me = new User( "My Name" );
//我们可以看到name已经被成为对象本身的属性
alert( me.name == "My Name" );
//而且它确实是User对象的一个新实例
alert( me.constructor == User );
//那么,既然User()只是一个函数,
//当我们这么处理它的时候,发生了什么?
User( "Test" );
//因为this上下文没有被设置,它缺省地指向全局的window对象,
//这意味着window.name将等于我们提供给它的那个name
alert( window.name == "Test" );
程序2-21. 使用constructor属性一例
复制内容到剪贴板
公有方法代码:
//创建一个新的、简单的User对象(函数)
function User() {}
//创建一个新的User对象
var me = new User();
//也是创建一个新的User对象(使用上前一个对象的constructor)
var you = new me.constructor();
//我们可以看到,实际上它们的constructor是同一个
alert( me.constructor == you.constructor );
公有方法可以完全地被对象的上下文中的最终使用者访问。为了实现这些对于特定对象的所有实例都可用的公共方法,你需要学习一个名为"prototype"的属性。prototype简单地包含一个对象,为一个父对象的所有新副本充当对基类的引用。本质上,prototype的任何属性对该对象的所每一个实例都是可用的。创建/引用的过程给了我们一个廉价版的继承,这一点我将在第三章论及。
由于对象的prototype也是一个对象,就跟其它任何对象一样,你可以给它附加新的属性。附加给prototype的新的属性将成为从原来的prototype对象实例化的每个对象的一部分,有效地使得该属性成为公有的(且可为全部实例所访问)。程序2-22展示一个此类例子:
程序2-22. 带有通过prototype附加的方法的对象的例子
复制内容到剪贴板
私有方法代码:
//创建一个新的User的构造器
function User( name, age ){
this.name = name;
this.age = age;
}
//为prototype对象添加一个新方法
User.prototype.getName = function(){
return this.name;
};
//为prototype对象添加另一个方法
//注意此方法的上下文将是被实例化的对象
User.prototype.getAge = function(){
return this.age;
};
//实例化一个新的User对象
var user = new User( "Bob", 44 );
//我们可以看到两个方法被附加到了对象上,有着正确的上下文
alert( user.getName() == "Bob" );
alert( user.getAge() == 44 );
私有方法和变量只能被其它的私有方法、私有变量的特权方法(下一节将会论述)访问。这是一种定义只能在内象内部访问的代码的方式。这一技术得益于Douglas Crockford的工作。他的网站提供了大量的详述面向对象的JavaScript的工作机制和使用方法的文档:
JavaScript文章列表:http://javascript.crockford.com/
文章"JavaScript中的私有成员":http://javascript.crockford.com/private.html
我们来看一个私有方法可以怎样应用中的例子,如程序2-23所示.
程序2-23. 私有方法只能被构造函数使用的示例:
复制内容到剪贴板
尽管很简单,私有方法却是非常重要的,它可以在保持你的代码免于冲突同时允许对你的用户可见和可用的施以更强大的控制。接下来,我们来研究特权方法。它是你的对象中可以使用的私有方法和共有方法的联合。代码:
//一个表示教室的对象构造器
function Classroom( students, teacher ) {
//用来显示教室中的所有学生的私有方法
function disp() {
alert( this.names.join(", ") );
}
//课程的数据存储在公有的对象属性里
this.students = students;
this.teacher = teacher;
//调用私有方法显示错误
disp();
}
//创建一新的教室对象
var class = new Classroom( [ "John", "Bob" ], "Mr. Smith" );
//失败,因为disp不是该对象的公有方法
class.disp();
特权方法
"特权方法"一语是Douglas Crockford创造的,用来称呼那种能够观察和维护私有变量而又可以作为一种公有方法被用户访问的方法。程序2-24展示了使用特权方法的一个例子。
程序2-24 使用特权方法一例
复制内容到剪贴板
本质上,特权方法是动态生成的方法,因为它们是在运行时而不是代码初次编译时添加给对象的。这种技术在计算量上要比绑定一个简单的方法到对象的prototype上来得昂贵,但同时也的强大和灵活得多。程序2-25展示了使用动态生成的方法可以实现什么。代码:
//创建一个新的User对象构造器
function User( name, age ) {
//计算用户的出生年份
var year = (new Date()).getFullYear() – age;
//创建一个新特权方法,对变量year有访问权,
//但又是公共可访问的
this.getYearBorn = function(){
return year;
};
}
//创建一个User对象的新实例
var user = new User( "Bob", 44 );
//验证返回的出生年份是否正确
alert( user.getYearBorn() == 1962 );
//并注意我们不能访问对象的私有属性year
alert( user.year == null );
程序2-25. 新对象初始化时创建的动态方法的示例
复制内容到剪贴板
(译注:这段程序是错误的。那个匿名函数里的this错误地指向了匿名函数的上下文,而其中的变量i却又恰仍属User 的上下文)代码:
//创建一个新的接受properties对象的对象
function User( properties ) {
//遍历对象属性,确保它作用域正确(如前所述)
for ( var i in properties ) { (function(){
//为属性创建获取器
this[ "get" + i ] = function() {
return properties[i];
};
//为属性创建设置器
this[ "set" + i ] = function(val) {
properties[i] = val;
};
})(); }
}
//创建一个新user对象实例,传入一个包含属性的对象作为种子
var user = new User({
name: "Bob",
age: 44
});
//请注意name属性并不存在,因为它在properties对象中,是私有的
alert( user.name == null );
//然而,我们能够使用用动态生成的方法getname来访问它
alert( user.getname() == "Bob" );
//最后,我们能看到,通过新生成的动态方法设置和获取age都是可以的
user.setage( 22 );
alert( user.getage() == 22 );
动态生成的代码的力量不可低估。能够基于变量的值实时的生成代码是极其有用;这与在其它语言(如Lisp)中宏那样强大的道理是一样的,不过是放在一种现代编程语言的背景里。接下来,我们将看到一类纯粹因其组织上的优势而有用的方法。
静态方法
静态方法背后的前提其实跟其它任何方法是一样的。然而,最主要的不同在于,这些方法作为对象的静态属性而存在。作为属性,它们在该对象的实例上下文中不可访问;它们只有在与主对象本身相同的上下文是可用的。这些与传统的类继承的相似点,使得他们有点像是静态的类方法。
实际上,以这种方式编写代码的唯一好处在于,这种方法保持对象名称空间的干净,——这一概念我就在第三章中更一步论述。程序2-26展示了附加在对象上的静态方法的一个例子。
程序2-26. 静态方法的简单示例
复制内容到剪贴板
静态方法是我们遇到的第一种纯粹以组织代码为目的的方法。这是向我们将要看到的下一章的重要过渡。开发专业品质JavaScript的一个基本侧观点,就是要有能力快速、平静地与其它代码段接口,同时保持可理解地可用性。这是一个重要的奋斗目标,也是我们下一章里所期望达到的。代码:
//附加在User对象上的一个静态方法
User.cloneUser = function( user ) {
//创建并返回一个新的User对象
return new User(
//该对象是其它user对象的克隆
user.getName(),
user.getAge()
);
};
本章摘要
理解本章概念的大纲的重要性是不容忽视的。本章的前半部分,让你对于JavaScript语言怎样运作和怎样最好地它用一个良好的理解,这是完全掌握专业地使用JavaScript的出发点。彻底地理解对象怎样运作、引用怎样处理、作用域怎样确定,将会毫无疑问地改变你编写JavaScript代码的方式。
有了广博的JavaScript编码技能,编写干净的面向对象JavaScript代码的重要性将会变得更加明显。本章的后半部分里我论述了怎样着手编写种种面向对象的代码以适应来自其它编程语言阵营的任何人。现代JavaScript正是基于这些技能,给予你开发新型的创新的应用程序时巨大的优势。