由于jQuery和MooTools等精心开发的库,JavaScript已成为前端开发的基础。不过,我们要留意这些优秀库中所运用的较高级概念,这点极其重要。原因何在?因为作为Web开发人员,对待学习最新的编程趋势和试图把那些趋势推向极致,我们必须予以一视同仁。要不然,Web开发领域就不会出现创新。所以,我们不妨花点时间来了解JavaScript面向对象编程的基本知识,包括类、继承和范围。
类
在我们学习如何把类实施到代码中之前,不妨讨论一下类是什么、为什么有必要学习/使用类。
正如Java文档声明的那样:“类是用来创建一个个对象的蓝图。”这蓝图就像造房子过程中所用的实际蓝图。建造人员使用蓝图来评估房子有什么样的属性,房子会有什么样的功能。类是表示对象属性的一种很方便的方式,无论这对象是房子、汽车还是人。当存在的某个对象不止一个时,类就变得特别有用。
比如说,我们不使用类来比较一下两个实际的对象。这体现了程序思考过程,而不是面向对象的思考过程。我们将描述一个名叫Rob的男子和一个名为Emillee的小女孩。我们必须假定我们对人体一无所知,因为我们没有蓝图(类)可供使用。
Rob:
1. Rob在身体的上部有两个椭圆形的结构,相隔几英寸。这些椭圆形结构有一个黑色背景,中间是棕色。
2. Rob有两个与地面相对平行的结构,似乎表明了人体中最垂直的部分,这仍是身体基部的一部分。
3. Rob有两个附属物,从另外两个附属物延伸过来。这些似乎可用来抓取物件。它们似乎比较大。
4. Rob高度约6英尺。
5. Rob无意识地吸入氧,把氧转换成二氧化碳。
Emilee:
1. Emillee在身体的上部有两个椭圆形的结构,相隔几英寸。这些椭圆形结构有一个黑色背景,中间是蓝色。
2. Emillee有两个与地面相对平行的结构,似乎表明了人体中最垂直的部分,这仍是身体基部的一部分。
3. Emillee有两个附属物,从另外两个附属物延伸过来。这些似乎可用来抓取物件。它们似乎比较小。
4. Emillee高度约1.5英尺。
5. Emillee无意识地吸入氧,把氧转换成二氧化碳。
单单描述一个人的1)眼睛、2)肩膀、3)双手、4)身高和5)呼吸行为就有大量的工作要做。要注意:我们不得不两次给出几乎一模一样的看法,因为我们没有蓝图可供使用。虽然描述两个人不是太费劲,但是如果我们想要描述100个人、1000个人或者100万个人,怎么办?肯定有一种更高效的方法来描述有着类似属性的对象:这正是类的亮点。
我们不妨使用面向对象的理念,重新考虑前一个例子。由于我们描述的是男子和小女孩,我们知道他们都是人类。所以不妨先为人类创建一个简单的蓝图。
人类:
1. 身体的上部有两个椭圆形的结构。这些椭圆形结构有一个黑色背景,中间颜色不一样。我们称之为眼睛。
2. 有两个与地面相对平行的结构,似乎表明了人体中最垂直的部分,这仍是身体基部的一部分。我们称之为肩膀。
3. 有两个附属物,从另外两个附属物延伸过来。这些似乎可用来抓取物件。它们的大小不一样。我们称之为双手。
4. 视年龄及其他因素而定,高度不一样。我们称之为身高。
5. 无意识地吸入氧,并把氧转换成二氧化碳。我们称之为呼吸。
于是我们已声明,人类的属性是,他们有眼睛,有肩膀,有双手,有身高。我们还已声明,这些属性可能不一样。定义了人类的蓝图后,并且声明了Rob和Emillee是人类后,我们可以将已经知道的关于人类的属性运用到Rob和Emillee。
Rob是人类。
1. Rob有棕色的眼睛
2. Rob有肩膀
3. Rob有大大的双手
4. Rob身高6英寸
5. Rob会呼吸
Emillee是人类。
1. Emillee有蓝色的眼睛
2. Emillee有肩膀
3. Emillee有小小的双手
4. Emillee身高1.5英尺
5. Emillee会呼吸
我们只要明确声明Rob和Emillee是人类,就可以把与人类有关的属性和功能直接运用到Rob和Emillee。这让我们可以避免重新定义身体的所有部位,同时让我们可以高效地描述这两个对象之间的重要区别。
下面是关于类及对象(名为类的实例)的几个例子,以便你明白两者之间的关系。
类Student(学生)
◆ 属性:年级、年龄、出生日期和学生身份标志(SSID)
◆ 功能:计算年级平均成绩、查看缺课情况、更新操行评语
类Employee(员工)
◆ 属性:雇主身份识别号(EIN)、小时工资、联系号码、保险
◆ 功能:设定薪水、查看工作效率和获取简历
类Computer(电脑)
◆ 属性:处理器、主机、显示器
◆ 功能:开机、关机和重启
好了,我们已明白了类背后的概念,不妨把所知道的东西运用到JavaScript。与包括PHP和C++在内的语言不一样,JavaScript没有类数据类型。不过,如果我们借助JavaScript的灵活性,就很容易使用函数来模拟类。
我们以前面一个例子为例,使用类来表示学生。
创建一个类时,你必须做两件事:必须知道这个类有什么属性/函数(又叫方法);你需要用一个值来初始化属性。
- function Student(name, gender, age, grade, teacher)
- {
- this.name = name;
- this.gender = gender;
- this.age = age;
- this.grade = grade;
- this.teacher = teacher;
- }
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.age); //输出15
- var susan = new Student("susan", "female", 10, 5, "Gresham");
- alert(susan.gender); //输出 'female'
我们可以从这个例子中看出,类的实例使用新的运算符来进行初始化。类的属性和方法使用. (dot)运算符来访问。所以为了获得名为bob的Student类实例的属性年龄,我们只要使用bob.age。同样,我们创建了Student类的一个实例,把它分配给susan。为了获得susan的性别,我们只要使用susan.gender。类在代码可读性方面带来了巨大的好处:你不需要有任何编程经验,就能推断出bob.age是bob的年龄。
不过,前一个例子有两个不好(但很容易修复)的缺点。
1)任何语句都可以访问类属性
2)参数必须以一定的次序来传递
#p#
确保属性值的私有性
请注意:在前一个例子中,我们只要调用bob.age,就能获得bob.age的值。此外,我们可以在程序中任何地方将bob.age设成自己喜欢的任何值。
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.age); //输出15
- bob.age = 9;
- alert(bob.age); //输出9;
看起来没有害处,是不是?那么请考虑这个例子。
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.age); //输出15
- bob.age = -50;
- alert(bob.age); //输出-50;
我们看到了年龄是负值:这在逻辑上不一致。我们只要使用私有变量(private variable)这个概念,就可以防止诸如此类的问题、确保数据的完整性。私有变量是只能在类本身里面访问的变量。虽然JavaScript再次没有用于确保变量私有性的保留字,但是JavaScript为我们提供了创造同样效果的工具。
- function Student(name, gender, age, grade, teacher)
- {
- var studentName = name;
- var studentGender = gender;
- var studentGrade = grade;
- var studentTeacher = teacher;
- var studentAge = age;
- this.getAge = function()
- {
- return studentAge;
- };
- this.setAge = function(val)
- {
- studentAge = Math.abs(val); //使用绝对值,确保年龄是正值
- };
- }
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.studentAge); //未定义,因为年龄在类定义中受私有保护
- alert(bob.getAge()); //输出15
- bob.setAge(-20);
- alert(bob.getAge()); //输出20
通过使用变量声明,而不是直接为类赋予属性,我们保护了年龄数据的完整性。由于JavaScript使用了函数范围,我们的类里面声明的变量在该类外面是无法访问的,除非由该类里面的函数明确返回。方法this.getAge将学生年龄返回到调用环境,它名为访问器方法(Accessor method)。访问器方法返回属性的值,那样该值就可以在类的外面使用,而不影响类里面的值。按照约定,访问器方法的前缀通常是“get”这个字。方法this.setAge名为更改器方法(Mutator method)。其目的是,改变属性的值,保护完整性。
我们已看到了在类里面使用访问器方法和更改器方法来保护数据完整性的好处。不过,为每个属性创建访问器方法带来了极其冗长的代码。
- function Student(name, gender, age, grade, teacher)
- {
- var studentName = name;
- var studentGender = gender;
- var studentGrade = grade;
- var studentTeacher = teacher;
- var studentAge = age;
- this.getName = function()
- {
- return studentName;
- };
- this.getGender = function()
- {
- return studentGender;
- };
- this.getGrade = function()
- {
- return studentGrade;
- };
- this.getTeacher = function()
- {
- return studentTeacher;
- };
- this.getAge = function()
- {
- return studentAge;
- };
- this.setAge = function(val)
- {
- studentAge = Math.abs(val); //使用绝对值,确保年龄是正值
- };
- }
- var bob = new Student("bob", "male", 15, 10, "Marlow");
- alert(bob.studentGender); //未定义,因为性别在类定义中受私有保护
- alert(bob.getGender()); //输出 'male'
教我C++的教授总是说:“如果你发现自己反复输入相同的代码,这表明你的做法不对。”的确,有更高效的方法可以为每个属性创建访问器方法。此外,这种机制还不需要以特定次序来调用函数参数。
动态创建的访问器方法
这个演示来自John Resig所写的《专业JavaScript技巧》一书(我强烈建议各位读一读。前三章就非常值得一读)。
- function Student( properties )
- {
- var $this = this; //将类范围存储到名为$this的变量中
- //迭代处理对象的属性
- for ( var i in properties )
- {
- (function(i)
- {
- // 动态创建访问器方法
- $this[ "get" + i ] = function()
- {
- return properties[i];
- };
- })(i);
- }
- }
- // 创建一个新的用户对象实例,并传递属性的对象
- var student = new Student(
- {
- Name: "Bob",
- Age: 15,
- Gender: "male"
- });
- alert(student.name); //因属性是私有的而未定义
- alert(student.getName()); //输出 "Bob"
- alert(student.getAge()); //输出15
- alert(student.getGender()); //输出 "male"
通过实施这个技巧,我们不但确保自己的属性是私有的,而且不需要按次序来指定参数。下面的类实例化都相同:
- var student = new Student(
- {
- Name: "Bob",
- Age: 15,
- Gender: "male"
- });
- var student = new Student(
- {
- Age: 15,
- Name: "Bob",
- Gender: "male"
- });
- var student = new Student(
- {
- Gender: "male",
- Age: 15,
- Name: "Bob"
- });
#p#
继承
在这篇文章中,我使用“类”这个术语极其宽松。如前所述,JavaScript没有类实体,但是后面仍可以跟类的模式。JavaScript与其他面向对象语言的区别主要在于继承模型。C++和Java体现了基于类的继承或传统继承。另一方面,JavaScript体现了原型继承(Prototypal Inheritance)。在其他面向对象语言中,类是一个实际的数据类型,表示创建对象的蓝图。在JavaScript中,虽然我们可以使用函数来模拟对象蓝图,但是它们实际上本身就是对象。然后,这些对象用作其他对象的模型(又叫原型),可以参阅文章《JavaScript原型继承》(http://www.webreference.com/programming/javascript/prototypal_inheritance/index.html)。
运用原型继承这个概念让我们得以创建“子类”,即继承另一个对象的属性的对象。如果我们想使用另一个对象的稍有一些变动的方法,这就变得极其有用。
以类Employee(员工)为例。假设我们有两种类型的员工:一种基于薪水,一种基于佣金。这些员工类型会有许多相似的属性。比如说,不管某员工通过佣金获得收入还是通过薪水获得收入,该员工都有名称。不过,对基于佣金的员工和基于薪水的员工来说,收入方式完全不一样。下面这个例子体现了这个概念:
- function Worker()
- {
- this.getMethods = function(properties, scope)
- {
- var $this = scope; //将类范围存储到名为$this的变量中
- //迭代处理对象的属性
- for ( var i in properties )
- {
- (function(i)
- {
- // 动态创建访问器方法
- $this[ "get" + i ] = function()
- {
- return properties[i];
- };
- //动态地创建一个分析整数,并确保是正值的更改器方法。
- $this[ "set" + i ] = function(val)
- {
- if(isNaN(val))
- {
- properties[i] = val;
- }
- else
- {
- properties[i] = Math.abs(val);
- }
- };
- })(i);
- }
- };
- }
- // CommissionWorker "子类"和WageWorker "子类"
- //继承Worker的属性和方法。
- CommissionWorker.prototype = new Worker();
- WageWorker.prototype = new Worker();
- function CommissionWorker(properties)
- {
- this.getMethods(properties, this);
- //计算收入
- this.getIncome = function()
- {
- return properties.Sales * properties.Commission;
- }
- }
- //要求有下列属性:薪水、每周小时数、每年周数
- function WageWorker(properties)
- {
- this.getMethods(properties, this);
- //计算收入
- this.getIncome = function()
- {
- return properties.Wage * properties.HoursPerWeek * properties.WeeksPerYear;
- }
- }
- var worker = new WageWorker(
- {
- Name: "Bob",
- Wage: 10,
- HoursPerWeek: 40,
- WeeksPerYear: 48
- });
- alert(worker.wage); //未定义。薪水是私有属性。
- worker.setWage(20);
- alert(worker.getName()); //输出 "Bob"
- alert(worker.getIncome()); //输出 38,400 (20*40*48)
- var worker2 = new CommissionWorker(
- {
- Name: "Sue",
- Commission: .2,
- Sales: 40000
- });
- alert(worker2.getName()); //输出 "Sue"
- alert(worker2.getIncome()); //输出8000(2% 乘40,000)
前一个例子中最重要的两个语句是:
- CommissionWorker.prototype = new Worker();
- WageWorker.prototype = new Worker();
这声明,对新的CommissionWorker或新的WageWorker对象的每个实例而言,Worker的属性和方法将传递到那些新对象。如果需要的话,这些方法和属性可以在“子类”定义里面被覆盖写入。
范围
JavaScript体现了所谓的函数范围。这意味着,函数中声明的变量在函数(变量来自该函数)外面最初是无法访问的。不过,在语句块中(如条件语句),可以对调用环境进行变量声明或改动。
- var car = "Toyota";
- if(car == "Toyota")
- {
- car = "Toyota - We never stop...and you won't either.";
- }
- alert(car); //输出Toyota——我们从未停上,你也如此。
- car = "Toyota"; //将汽车设回成原始值。
- function makeFord(car)
- {
- car = "Ford";
- }
- makeFord(car);
- alert(car); //输出"Toyota",因为汽车在函数范围中已改动。
不过,如果你想要改动值的函数,可以将对象作为参数来传递,并改动对象的属性。
- var car = new Object();
- car.brand = "Toyota"
- function makeFord(car)
- {
- car.brand = "Ford";
- }
- makeFord(car);
- alert(car.brand); //输出“Ford”
这名为“通过调用”传递,将值传递给函数。只有你在类里面创建方法,又知道对象含有什么属性,我一般才会建议采用通过调用传递。
现在你已经掌握了运用到JavaScript的面向对象的基本知识。运用这些原则,就可以为你在将来的开发项目简化代码。
原文:http://www.1stwebdesigner.com/design/object-oriented-basics-javascript/
【编辑推荐】