语言特性 引用 JavaScript的一个重要的方面是引用的概念。引用就是指向对象实际位置的指针。这是一项极其强大的功能。前提是,实际的对象决不是一个引用:字符串总是一个字符串,数组总是一个数组。然而,多个变量可以引用相同的对象。JavaScript就是以这种引用引用机制为基础。通过维护一系列的指向其它对象的引用,语言为你提供了更大的弹性。
另外,对象能包括一系列的属性,这些属性简单地引用其它对象(如字符串,数字,数组等等)。当几个变量指向相同对象时,修改底层对象类型将会在所有的指点向它的变量上有所反映。例2-1即此一例,两个变量指向同一个对象,但是对对象内容的修改的反映是全局的。
程序2-1. 多变量引用单个对象的示例
//设置obj为一个空对象
var obj = new Object();
//objRef现在引用了别的对象
var objRef = obj;
//修改原始对象的属性
obj.oneProperty = true;
//我们可以发现该变化在两个变量中都可以看到
//(因为他们引用了同一个对象)
alert( obj.oneProperty === objRef.oneProperty );
我从前提到过自更改的对象在JavaScript里非常少见的。让我们看一个发生这一状况的实例。数组对象能够用push方法给它自己增加额外的项。因为在数组对象的核心,值是作为对象的属性存储的,结果类似程序2-1中的情形,一个对象成为全局被改动的(导致了多个变量的值被同时改变)。见程序2-2.
程序2-2. 自修改对象的例子
//创建一组项目的数组
var items = new Array( "one", "two", "three" );
//创建一个对项目数组的引用
var itemsRef = items;
//给原始数组添加一项
items.push( "four" );
//两个数组的长度应该相同,
//因为它们都指向相同的数组对象
alert( items.length == itemsRef.length );
记住这一点是很重要的:引用总是只指向最终被引用的对象,而不会是引用本身。例如,在Perl语言里,很可能有一个引用指向另一个也是引用的变量。但在JavaScript里,它会沿着引用链向下追溯直到指向核心的对象。程序2-3演示了这种情形,物理的目标已经改变而引用仍然指向原来的对象。
程序2-3. (见#9 oerrite 的回复)
// 设置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中可以看出。
程序2-4. 对象修改作用于一个新的对象而非自修改对象的示例
//让item等于一个新的字符串对象
var item = "test";
//itemRef也引用相同的字符串对象
var itemRef = item;
//在字符串对象上串联一个新的对象
//注意:这创建了一个新的对象,并不修改初始对象
item += "ing";
//item和itemRef的值并不相等,因为
//一个全新的对象被创建了
alert( item != itemRef );
如果你刚刚接触,引用可能是个令人头大的刁钻话题。然而,理解引用是如何工作的对于编写良好、干净的JavaScript代码是极其重要的。接下来的几节我们将探究几种未必新鲜和令人激动的,但是同样对编写良好、干净的代码很重要的特性。
函数重载和类型检查 其它面向对象的语言(比如Java)的一种共有的特性是“重载”函数的能力:传给它们不同数目或类型的参数,函数将执行不同操作。虽然这种能力在JavaScript中不是直接可用的,一些工具的提供使得这种探求完全成为可能。
在JavaScript的每一个函数里存在一个上下文相关的名为arguments的变量,它的行为类似于一个伪数组,包含了传给函数的所有参数。参数不是一真正的数组(意味着你不能修改它,或者调用push()方法增加新的项),但是你可以以数组的形式访问它,而且它也的确有一个length属性。程序2-5中有两个示例。
程序2-5. JavaScript中函数重载的两个示例
//一个简单的用来发送消息的函数
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;
}
另外,存在另一种断定传递给一个函数的参数数目的方法。这种特殊的方法多用了一点点技巧:我们利用了传递过来的任何参数值不可能为undefined这一事实。程序2-6展示一了个简单的函数用来显示一条错误消息,如果没有传给它,则提供一条缺省消息。
程序2-6: 显示错误消息和缺省消息
function displayError( msg ) {
//检查确保msg不是undefined
if ( typeof msg == 'undefined' ) {
//如果是,则设置缺省消息
msg = "An error occurred.";
}
//显示消息
alert( msg );
}
typeof语句的使用引入了类型检查。因为JavaScript(目前)是一种动态类型语言,使得这个话题格外有用而重要的话题。有许多种方法检查变量的类型;我们将探究两种特别有用的。
第一种检查对象类型的方式是使用显式的typeof操作符。这种有用的方法给我们一个字符串名称,代表变量内容的类型。这将是一种完美的方案,除非变量的类型或者数组或自定义的对象如user(这时它总返回"ojbect",导致各种对象难以区分)。
这种方法的示例见程序2-7
程序2-7. 使用typeof决定对象类型的示例
//检查我们的数字是否其实是一个字符串
if ( typeof num == "string" )
//如果是,则将它解析成数字
num = parseInt( num );
//检查我们的数组是否其实是一个字符串
if ( typeof arr == "string" )
//如果是,则用逗号分割该字符串,构造出一个数组
arr = arr.split(",");
检查对象类型的第二种方式是参考所有JavaScript对象所共有的一个称为constructor的属性。该属性是对一个最初用来构造此对象的函数的引用。该方法的示例见程序2-8。
程序2-8. 使用constructor属性决定对象类型的示例
//检查我们的数字是否其实是一个字符串
if ( num.constructor == String )
//如果是,则将它解析成数字
num = parseInt( num );
//检查我们的字符串是否其实是一个数组
if ( str.constructor == Array )
//如果是,则用逗号连接该数组,得到一个字符串
str = str.join(',');
表2-1显示了对不同类型对象分别使用我所介绍的两种方法进行类型检查的结果。表格的第一列显示了我们试图找到其类型的对象。每二列是运行typeof Variable(Variable为第一列所示的值)。此列中的所有结果都是字符串。最后,第三列显示了对第一列包含的对象运行Variable.constructor所得的结果。些列中的所有结果都是对象。
表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. 一个可用来严格维护全部传入函数的参数的函数
//依据参数列表来严格地检查一个变量列表的类型
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中一个较难处理的特性。所有面向对象的编程语言都有某种形式的作用域;这要看是什么上下文约束着作用域。在JavaScript里,作用域由函数约束,而不由块约束(如while,if,和for里的语句体)。最终可能使得一些代码的运行结果表面上显得怪异(如果你来自一种块作用域语言的话)。程序2-10的例子说明了“函数作用域代码”的含义。
代码2-10. JavaScript中变量作用域是怎样工作的例子
//设置一个等于"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-10中你会发现,变量位于在全局作用域。基于浏览器的JavaScript有趣的一面是,所有的全局变量实际上都是window对象的属性。尽管一些老版本的Opera浏览器或Safari浏览器不是这样,假定浏览器这样工作通常是一个很好的经验规则。程序2-11展示了一个这种例子。
程序2-11. JavaScript的全局变量与window对象的例子
//全局变量,包含字符串"test"
var test = "test";
//你会发现,我们的全局变量和window的test属性是相同的
alert( window.test == test );
最后,让我们来看看当一个变量漏定义时会怎样。程序2-12里,变量foo在test()的作用域里被赋值。但是,程序2-12里实际并没有(用var foo)定义变量的作用域。当变量foo没有明确定义时,它将成为全局变量,即使它只在函数的上下文使用。
程序2-12. 隐式全局变量声明的示例
//一个为变量foo赋值的函数
function test() {
foo = "test";
}
//调用函数为foo赋值
test();
//我们发现foo现在是全局变量了
alert( window.foo == "test" );
到目前应该很明显,尽管JavaScript的作用域不如块作用域语言的严格,它还是相当强大和有特色的。尤其是与下节中叙述的闭包的概念结合起来时,JavaScript语言的强大将展露无遗。
闭包 闭包意味着内层的函数可以引用存在于包绕它的函数的变量,即使外层的函数的执行已经终止。这一特殊的论题可能是非常强大又非常复杂的。我强烈推荐你们参考本节后面将提及的站点,因为它有一些关于闭包这一话题的精彩的信息。
我们先来看程序2-13所示的闭包的两个简单例子。
程序2-13. 闭包改善的代码清晰性的两例
//得到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的函数调用,展示了一个的JavaScript新手遇到问题的通俗的例子。在JavaScript新手的程序里像这样的代码时常可以看到:
setTimeout("otherFunction()", 1000);
//或者甚至
setTimeout("otherFunction(" + num + "," + num2 + ")", 1000);
使用闭包的概念,完全可能的把这种混乱的代码清理掉。第一个例子很简单;有一个回调函数在调用setTimeout函数以后1000微秒以后被调用,而它仍引用了变量obj(定义在全局范围,指向id为"main"的元素)。定义的第二个函数,delayedAlert,展示了一种解决出现的setTimeout混乱的方案,以及函数作用域内可以有闭包的能力。
你们应该可以发现,当在代码中使用这种简单的闭包时,你所写的东西的清晰性将会提高,免于陷入语法的迷雾之中。
我们来看一个闭包可能带来的有有趣的副作用。在某些函数化的编程语言里,有一个叫做
currying的概念。本质上讲,currying是就是为函数的一些参数预填入值,创建一个更简单的新函数的方法。代码2-14里有一个简单的currying的例子,创建了向另一个函数预填一个参数而得的新函数。
代码2-14. 使用闭包的函数currying
//生成做加法的新函数的函数
function addGenerator( num ) {
//返回一个简单函数用来计算两个数的加法,
//其中第一个数字从生成器中借用
return function( toAdd ) {
return num + toAdd
};
}
//addFive现在是接受一个参数的函数,
//此函数将给参数加5,返回结果数字
var addFive = addGenerator( 5 );
//这里我们可以看到,当传给它参数4的时候
//函数addFive的结果为9
alert( addFive( 4 ) == 9 );
闭包还能解决另一个常见的JavaScript编码方面的问题。JavaScript新手趋向于在全局作用域里放置许多变量。这一般被认为是不好的习惯,因为那些变量可能悄悄地影响其它的库,导致令人迷惑的问题的产生。使用一个自执行的、匿名的函数,你可以从根本上隐藏所有的通常的全局变量,使它们对其它代码不可见,如程序2-15所示。
代码2-15. 使用匿名函数从全局作用域隐藏变量的例子
//创建一个用作包装的匿名函数
(function(){
//这个变量通常情况下应该是全局的
var msg = "Thanks for visiting!";
//为全局对象绑定新的函数
window.onunload = function(){
//使用了“隐藏”的变量
alert( msg );
};
//关闭匿名函数并执行之
})();
最后,让我们来看使用闭包时出现的一个问题。闭包允许你引用存在于父级函数中的变量。然而,它并不是提供该变量创建时的值;它提供的是父级函数中该变量最后的值。你会看到这个问题最通常是在一个for循环中。有一个变量被用作迭代器(比如i),在for内部新的函数被创建,并使用了闭包来引用该迭代器。问题是,当新的闭包函数被调用时,它们将会引用该iterator最后的值(比如,一个数组的最后位置),而不是你所期望的那个。程序2-16的例子说明,使用匿名函数激发作用域,在其中创建一个合乎期望的闭包是可能的。
程序2-16. 使用匿名函数激发一个创建多个闭包函数所需的作用域的例子
//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中的闭包是怎么工作的:Jim Jey的"JavaScript闭包",网址是
https://jibbering.com/faq/faq_notes/closures.html。
最后,我们将研究上下文的概念,这是许多JavaScript的面向对象特性赖以建立的基石。
上下文 在JavaScript中,你的代码将总是有着某种形式的上下文(代码在其内部工作的对象)。这也是其它面向对象语言所共有的功能,但它们都不如JavaScript处理得这样极端。
上下文是通过变量this工作。变量this总是引用代码当前所在的那个对象。记住全局对象实际上是window对象的属性。这意味着即使是在全局上下文里,this变量仍然引用一个对象。上下文可以成为一个强大的工具,是面向对象代码不可或缺的一环。程序2-17展示了一些关于上下文的简单例子。
程序2-17. 在上下文中使用函数然后将其上下文切换到另一个变量的例子
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-17中,当我们切换obj.no方法的上下文到变量window时,笨重的代码需要切换函数的上下文。幸运的是,JavaScript提供了两种方法使这一过程变得更加易于理解和实现。程序2-18展示了恰能些目的的两种不同方法,call和apply。
程序2-18. 改变函数上下文的示例
//一个简单的设置其上下文的颜色风格的函数
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"时,它会变得更加明显。
[
本帖最后由 mozart0 于 2007-4-8 12:41 编辑 ]