转载:
对象是JavaScript的基本单位。实际上JavaScript中一切都是对象并得益于这一事实。然而,为了增强这一纯粹的面向对象的语言,JavaScript包括了一个庞大的功能集,使它无论是在潜在能力还是风格上,都成为一门极其独特的语言。
本章中我将开始覆盖JavaScript语言的最重要的一些方面,如引用,作用域,闭包,以及上下文,你会发现这正是其它JavaScript书籍中很少论及的。打下主要的基础以后,我们将开始探索面向对象JavaScript的几个重点,包括对象到底如何运作和怎样创建新的对象并在特定的许可条件下设置其方法。如果你认真去读的话,这很可能是本书中最重要的一章,它将彻底地改变你看待JavaScript作为一门编程语言的方式。
语言特性
引用
JavaScript的一个重要的方面是引用的概念。引用就是指向对象实际位置的指针。这是一项极其强大的功能。前提是,实际的对象决不是一个引用:字符串总是一个字符串,数组总是一个数组。然而,多个变量可以引用相同的对象。JavaScript就是以这种引用引用机制为基础。通过维护一系列的指向其它对象的引用,语言为你提供了更大的弹性。
另外,对象能包括一系列的属性,这些属性简单地引用其它对象(如字符串,数字,数组等等)。当几个变量指向相同对象时,修改底层对象类型将会在所有的指点向它的变量上有所反映。例2-1即此一例,两个变量指向同一个对象,但是对对象内容的修改的反映是全局的。
程序2-1. 多变量引用单个对象的示例
代码:
//设置obj为一个空对象
var obj = new Object();
//objRef现在引用了别的对象
var objRef = obj;
//修改原始对象的属性
obj.oneProperty = true;
//我们可以发现该变化在两个变量中都可以看到
//(因为他们引用了同一个对象)
alert( obj.oneProperty === objRef.oneProperty );
程序2-2. 自修改对象的例子
代码:
//创建一组项目的数组
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 的回复)
代码:
// 设置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. 对象修改作用于一个新的对象而非自修改对象的示例
代码:
//让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中函数重载的两个示例
代码:
//一个简单的用来发送消息的函数
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: 显示错误消息和缺省消息
代码:
function displayError( msg ) {
//检查确保msg不是undefined
if ( typeof msg == 'undefined' ) {
//如果是,则设置缺省消息
msg = "An error occurred.";
}
//显示消息
alert( msg );
}
第一种检查对象类型的方式是使用显式的typeof操作符。这种有用的方法给我们一个字符串名称,代表变量内容的类型。这将是一种完美的方案,除非变量的类型或者数组或自定义的对象如user(这时它总返回"ojbect",导致各种对象难以区分)。
这种方法的示例见程序2-7
程序2-7. 使用typeof决定对象类型的示例
代码:
//检查我们的数字是否其实是一个字符串
if ( typeof num == "string" )
//如果是,则将它解析成数字
num = parseInt( num );
//检查我们的数组是否其实是一个字符串
if ( typeof arr == "string" )
//如果是,则用逗号分割该字符串,构造出一个数组
arr = arr.split(",");
程序2-8. 使用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. 一个可用来严格维护全部传入函数的参数的函数
代码:
//依据参数列表来严格地检查一个变量列表的类型
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中变量作用域是怎样工作的例子
代码:
//设置一个等于"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对象的例子
代码:
//全局变量,包含字符串"test"
var test = "test";
//你会发现,我们的全局变量和window的test属性是相同的
alert( window.test == test );
程序2-12. 隐式全局变量声明的示例
代码:
//一个为变量foo赋值的函数
function test() {
foo = "test";
}
//调用函数为foo赋值
test();
//我们发现foo现在是全局变量了
alert( window.foo == "test" );
闭包
闭包意味着内层的函数可以引用存在于包绕它的函数的变量,即使外层的函数的执行已经终止。这一特殊的论题可能是非常强大又非常复杂的。我强烈推荐你们参考本节后面将提及的站点,因为它有一些关于闭包这一话题的精彩的信息。
我们先来看程序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("otherFunction()", 1000);
//或者甚至
setTimeout("otherFunction(" + num + "," + num2 + ")", 1000);
你们应该可以发现,当在代码中使用这种简单的闭包时,你所写的东西的清晰性将会提高,免于陷入语法的迷雾之中。
我们来看一个闭包可能带来的有有趣的副作用。在某些函数化的编程语言里,有一个叫做 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 );
代码2-15. 使用匿名函数从全局作用域隐藏变量的例子
代码:
//创建一个用作包装的匿名函数
(function(){
//这个变量通常情况下应该是全局的
var msg = "Thanks for visiting!";
//为全局对象绑定新的函数
window.onunload = function(){
//使用了“隐藏”的变量
alert( msg );
};
//关闭匿名函数并执行之
})();
程序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的面向对象特性赖以建立的基石。
上下文
在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-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"这一说法多少有些冗余,因为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. 创建并使用一个简单的对象
代码:
//一个简单的函数,接受一个参数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 使用特权方法一例
代码:
//创建一个新的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. 新对象初始化时创建的动态方法的示例
代码:
//创建一个新的接受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. 静态方法的简单示例
代码:
//附加在User对象上的一个静态方法
User.cloneUser = function( user ) {
//创建并返回一个新的User对象
return new User(
//该对象是其它user对象的克隆
user.getName(),
user.getAge()
);
};
本章摘要
理解本章概念的大纲的重要性是不容忽视的。本章的前半部分,让你对于JavaScript语言怎样运作和怎样最好地它用一个良好的理解,这是完全掌握专业地使用JavaScript的出发点。彻底地理解对象怎样运作、引用怎样处理、作用域怎样确定,将会毫无疑问地改变你编写JavaScript代码的方式。
有了广博的JavaScript编码技能,编写干净的面向对象JavaScript代码的重要性将会变得更加明显。本章的后半部分里我论述了怎样着手编写种种面向对象的代码以适应来自其它编程语言阵营的任何人。现代JavaScript正是基于这些技能,给予你开发新型的创新的应用程序时巨大的优势。