2.2 面向对象基础
面向对象JavaScript这个词其实有些多余,因为JavaScript这门语言就是完全面向对象的,也不可能以非面向对象的方法来使用。不过大多数编程新手(包括使用JavaScript的)的常见弱点在于按照功能编写代码,而不考虑任何上下文或者组织。要完整理解如何编写最优化的JavaScript代码,就必须理解JavaScript对象是如何工作的,它们和其他语言的对象有何不同,以及怎样使用才对你有益。
本章的后半部分里,我们将介绍编写面向对象JavaScript代码的基础,然后在后续章节讨论以这种方式编写代码的可行性。
2.2.1 对象
对象是JavaScript的基础。事实上,这门语言里所有的东西都是对象。这门语言的大部分功用都是基于这一点的。从最基本的层次上说,对象是一系列属性的集合,和其他语言里的散列表结构类似。代码清单2-19是展示创建有多个属性的对象的两个例子。
代码清单2-19 两个创建简单对象并设置属性的例子
// 创建一个新的 Object 对象,存放在 'obj' 变量中
var obj = new Object();
// 给此对象设置一些属性
obj.val = 5;
obj.click = function(){
alert( "hello" );
};
// 这是一段等价代码,用 {...} 简写方式,结合键值对(key/value pair)来定义属性
var obj = {
// 用键值对方式来设置属性名和属性值
val: 5,
click: function() {
alert( "hello" );
}
};
2.2.2 对象的创建
和大部分的其他面向对象语言不同的是,JavaScript并没有类(class)的概念。其他面向对象语言中你大多需要实例化某个具体类的实例,但JavaScript里不用。JavaScript里对象本身可以用来创建新对象,而对象也可以继承自其他对象。这个概念称为原型化继承(prototypal inheritance),会在之后的“公共方法”一节作更详细地讨论。
不过不管JavaScript使用何种对象方案,首先还是应该有一种创建新对象的方法的。JavaScript的做法是,任何函数都可以被实例化为一个对象。实际上,这个方法用起来并没有听起来这么令人迷惑。这很像把一块面团放进烤甜饼模具里,再切成一块一块的,其中面团是原对象,模具就是使用对象原型的构造函数。
让我们看看代码清单2-20里的例子,以了解它是如何运作的。
代码清单2-20 简单对象的创建和使用
// 一个简单的函数,接受名称并将其存入当前上下文中
function User( name ) {
this.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 等于提供的这个名字
alert( window.name == "Test" );
代码清单2-20展示了constructor属性的使用,这一属性在每个对象中都存在,并一直指向创建它的函数。这样一来你就可以有效地复制对象了,用同一个基类创建对象并赋予不同的属性。这种方式的一个例子如代码清单2-21所示。
代码清单2-21 使用constructor属性的例子
// 创建一个新的简单的 User 对象
function User() {}
// 创建一个 User 对象
var me = new User();
// 还是创建一个新的 User 对象(用前一个对象的 constructor 引用来创建)
var you = new me.constructor();
// 你可以发现这两个对象的 constructor 实质上是一致的
alert( me.constructor == you.constructor );
现在我们知道如何创建简单对象了,是时候添加一些让对象更有用的东西了——上下文相关方法(contextual method)和属性。
1.公共方法
公共方法(public method)在对象的上下文中是最终用户始终可以接触到的。要实现这种在对象的每个实例中都可以使用的公共方法,必须了解一个叫prototype(原型)的属性,这个属性包含了一个对象,该对象可以作为所有新副本的基引用(base reference)。本质上说,所有对象原型的属性都能在该对象的每个实例中找到。这种创建/引用的过程带来了一种继承的简单版本,我们会在第3章讨论它。
因为对象的原型仍然是对象,和其他任何对象一样,你也可以给它们添加新的属性。给原型添加属性的结果是由该原型实例化的每个对象都会获得这些属性,也就使这些属性公有化了(能被所有对象访问)。代码清单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 );
目前大多数JavaScript开发者开发新应用程序时已经掌握简单的构造函数和简单的原型对象处理了。在本节后半部分,将介绍一系列让你充分发挥面向对象代码功用的其他技巧。
2.私有方法
私有方法(private method)和私有变量只允许其他的私有方法、私有变量和特权方法(下一小节讨论)访问。这种方法可以定义一些只让对象内部访问,而外部访问不到的代码,这一技巧来自Douglas Crockford的努力,他的网站提供了大量的文档,详细介绍面向对象的JavaScript如何运作及应该如何使用:
l JavaScript 文章一览:http://javascript.crockford.com/。
l Private Members in 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();
}
// 创建一个新的 classroom 对象
var class = new Classroom( [ "John", "Bob" ], "Mr. Smith" );
// 调用disp方法会失败,因为它不是该对象的公共属性
class.disp();
虽然很简单,但私有方法和私有变量在保证代码没有冲突的同时,允许你对用户能使用和能看到的内容有更好的控制。下一步,我们将了解特权方法,它是私有方法和公共方法的混合体。
3.特权方法
特权方法(privileged method)是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 );
// 注意我们无法访问该对象私有的年份属性
alert( user.year == null );
本质上,特权方法是动态生成的,因为它们是在运行时才添加到对象中的,而不是在代码第一次编译时就已经生成的。虽然这个技巧要比往对象的prototype上绑定一个简单的方法开销更大,但功能也更强大、更灵活。代码清单2-25是动态生成方法能实现的一个例子。
代码清单2-25 动态生成方法的例子,这些方法在新对象实例化时创建
// 创建一个新的用户对象,接受一个有许多属性的对象作为参数
function User( properties ) {
// 遍历该对象的所有属性,并保证其作用域正确(如前面所述)
for ( var i in properties ) { (function(which){
var p = i;
// 创建此属性的一个新的读取器 (getter)
which[ "get" + p ] = function() {
return properties[p];
};
// 创建此属性的一个新的设置器 (setter)
which[ "set" + p ] = function(val) {
properties[p] = val;
};
})(this);
}
}
// 创建一个新的用户对象实例,并把具有两个属性的一个对象传入作为种子
var user = new User({
name: "Bob",
age: 44
});
// 注意 name 属性并不存在,
// 因为它是属性对象(properties object)的私有变量
alert( user.name == null );
// 不过我们可以使用新的 getname() 方法来获得这个值
// 因为此函数是动态生成的
alert( user.getname() == "Bob" );
// 最后,我们看到能够使用这个新生成的函数来设置或获得年龄
user.setage( 22 );
alert( user.getage() == 22 );
动态生成代码的能力不可小视,能够根据运行时变量来生成代码是非常有用的,这也是其他语言(如Lisp)里的宏(macro)如此强大的原因,不过现在是以现代程序设计语言的形式来表达罢了。下一步我们将看到一种只在组织代码时有用的方法类型。
4.静态方法
静态方法的实质与任何其他一般函数没有什么不同,最主要的区别在于,其他函数是以对象的静态属性形式存在的。作为一个属性,它们不能在该对象的实例的上下文中访问,而只属于主对象本身的那个上下文中。对习惯了传统类式继承的人来说,这就像类里定义的静态方法。
实际上,这样编写代码的唯一优点是保证对象的命名空间整洁,第3章会对这个概念作更详细讨论。代码清单2-26展示了一个把静态方法添加到对象中的例子。
代码清单2-26 静态方法的一个简单例子
// 添加到一个 User 对象的静态方法
User.cloneUser = function( user ) {
// 创建并返回一个新的用户
return new User(
// 这是其他用户对象的复制
user.getName(),
user.getAge()
);
};
静态方法是我们遇到的第一种仅为组织代码而使用的方法。由此我们将转入下一章所要讨论的内容。开发出专业JavaScript代码的根本方法之一是,快速、静态地提供与其他代码的接口,同时保证自身的可理解性。这是一个值得努力的重要目标,也是我们要在下一章要达到的目标。






