1、 JavaScript 面向对象程序设计前言基于对象还是面向对象?面向对象技术是现代软件开发中的重要技术之一。面向对象变成的好处毋庸置疑,现在的主流语言如 Java、C+都是面向对象的。现在的面向对象理论更多的是使用 Java 或 C+进行描述,究其根源,在于这些语言都是传统的面向对象语言,具有面向对象理论所指明的一切特性:类、封装、继承、多态等等。相比而言,一些动态语言如 JavaSript 就显得不那么面向对象至少,在 JavaScript 中并没有类 class 这一关键字。但是,在 JavaScript 中并不是没有类的概念。于是有人说,JavaScript 是基于对象的语言,而不是面
2、向对象的语言。面向对象的语言具有三个特性:封装、继承和多态,三者缺一不可;基于对象的语言通常仅仅是使用对象,其实现的是封装,并没有提供后两种特性。的确,从语法上来说,JavaScript 并没有特定的语法或者在语言级别上来实现继承和多态。但是,这并不妨碍我们使用这些特性。这是因为,JavaScript 是一种灵活的语言,它是相当的灵活,以至于这些并没有提供的东西,更确切的说,是没有明确的表明的东西,都是可以实现和使用的!那么,你还能说 JavaScript 是基于对象而不是面向对象的吗?面向对象也是一种思想,任何语言,包括 C 语言,同样可以使用面向对象的思想去解决现实生活中的各种问题。到底是
3、基于对象还是面向对象,这些概念让计算机哲学家门去争论吧相信他们的争论最终也会和先有鸡还是先有蛋的问题一样的结果我们所要做的,是要使用这种语言提供的机制去解决我们的问题。为什么要有 JavaScript 的面向对象编程?这个问题很严肃 这取决你问题的规模和应用的范围。就像 JavaEE 和 PHP 一样:PHP能实现的东西,JavaEE 都能实现,那么,为什么还要有 PHP?因为 JavaEE 太复杂了,对于一些简单的系统,根本没有必要是使用它,也就是所谓的“杀鸡焉用牛刀”。JavaScript 主要应用于 Web 开发中。在传统的 Web 开发模式中,JavaScript 起到的是一些点缀的作
4、用,只完成很有限的功能,例如表单验证等。于是,JavaScript 多被当做一种过程性语言使用,很难完成复杂的功能。而今天 Web2.0 的时代, Ajax 大行其道,很多复杂的脚本成为其必须的组成部分。在 Ajax 应用中利用 JavaScript 面向对象编程风格,能够使逻辑更加清晰,也更有利于问题的解决。如果你想用 JavaScript 编写一个库,比如 ExtJS 或者 YUI,很难想象你的类库不使用面向对象的编程风格否则的话,无论是对你还是对使用者的智力都将是一个前所未有的考验!或许,自从面向对象思想提出之后,已经很难有类库不使用面向对象的方式实现了,即便是 C 语言的库诸如 gtk
5、+,也是用 C 语言将面向对象的思想表现的天衣无缝。面向对象的思想对于大型程序的编写和使用具有不可替代的作用。本系列文章将试图向读者阐述 JavaScript 的面向对象程序设计。尽管 JavaScript 中具有很多浏览器相关的概念,如 document 等内置对象,但是本系列将不涉及这些问题,并且将假设读者已经有 JavaScript 基础的语法知识等。本系列文章不会从头开始讲述 JavaScript 的语法,仅仅从纯粹的面向对象角度审视 JavaScript,或许,你将会看到一个教程:面向对象程序设计JavaScript 语言描述。这才是本系列文章的目的。数组或许你会奇怪,面向对象的程序
6、设计为什么从数组开始讲起?这是因为其间的种种关系吧嘿嘿,这里先卖个关子,先来看看我们熟悉的数组在 JavaScript 里面是什么样子的。1. 创建数组在 JavaScript 中有很多创建数组的方法。比如使用 Array 函数。不过这不是现在我们要讲述的。现在我们使用简单的方括号“”的办法来创建数组。 var objAyyar = ; / 1var objAyyar = 2; / 2var objAyyar = “a“, “b“, “c“; / 3var objAyyar = new Date(), 123, “abc“; / 4复制代码这里有四个创建数组的语句。下面来一一解释一下:第一句,
7、创建一个空的数组;第二句,创建一个数组,数组元素只有一个 2;第三句,创建一个数组,数组的元素分别初始化为“a“, “b“, “c“;第四句,创建一个数组,其中第一个元素为一个 Date 类型的对象,第二个元素是数字123,第三个元素是字符串“abc“。回顾一下,在 Java 或者 C+语言中,数组是具有相同的数据类型的元素的集合。比如使用Java 语言的下面语句 int array = new int10;复制代码将创建一个能放入 10 个 int 类型的元素的数组。数组和其他类型的集合的一个很大的区别是,数组里面只能存放相同数据类型的元素(使用泛型的集合除外) 。但是,像上面的第四句,Ja
8、vaScript 的数组怎么能存放不同类型的元素呢?这是因为,JavaScript 是弱类型的语言,没有很大的数据类型的差别,所以数组的元素可以放入不同的类型。2. 操作数组数组是元素的有序集合。数组中的元素是有序的,这就可以通过下标访问到数组中的每个元素。而且,JavaScript 的数组相当的灵活。当你习惯了 Java 或者 C+的数组之后,或许并不习惯 JavaScript 的数组。在一定程度上,这种数组可以称为一种动态数组。看这样一段代码: var arr = 1, 2, 3, 4, 5; alert(arr.length); / 数组长度为 5alert(arr3); / arr3
9、= 4arr9 = 10; / 改变了数组的长度为10alert(arr7);alert(arr.length);复制代码首先创建一个数组 arr,可以看到它的长度是 5,arr3 是 4。这些都是很常见的。那么第三句,arr9 = 10;就有点意思了在 Java 中,这句操作将导致数组越界的异常,在 C+中,这种操作是极其危险的。但是在 JavaScript 中,这样的操作是正常的你可以动态的改变数组的大小!虽然你在创建数组时并没有这么大的长度,但是,你可以在创建之后指定它!这时的 arr.length 已经自动的变成 10 了。那么,arr7又会是什么呢?经过运行代码我们会看到,arr7是
10、 undefined。也就是说,虽然 arr9有了值,但是其中从 arr5到 arr8这几个元素都是未定义的,也就是 undefined。如果你问 JavaScript 怎么不给个初始值?唉,饶了它吧!JavaScript 并不知道你想要它初始化成什么值啊!万一错了呢?干脆还是别了吧 var arr = 1, 2, 3, 4, 5; alert(arr.length); / 数组长度为 5delete arr3; / 删掉第4个元素alert(arr.length); / 长度不变alert(arr3); / arr3 = undefinedarr.length = 4; / 缩短长度aler
11、t(arr4);arr.length = 10; / 增加长度alert(arr6);复制代码上面的代码也很有意思:使用 delete 操作符可以删除任意一个数组元素,但是长度并不改变。Java 的数组也有一个 length 属性,用来显示数组的长度。JavaScript 的数组也有这个属性。但是,和 Java 不同的是,后者的 length 属性并不是只读的!你可以任意的设置数组的length 属性的值,无论是扩大还是缩小!只是如上面的代码所示,改变了 length 之后,越界的元素或者以前没有定义的元素都将成为 undefined。也就是说,当 length 大于原始长度时,从原长度到 l
12、ength - 1 的元素都将成为 undefined;当 length 小于原始长度时,从 length到原长度 - 1 的元素也都会清除设置为 undefined。3. 非数字的下标?如果动态的 length 属性还不够灵活的话,那么,JavaScript 的数组还有另外的能力。你见到过用字符串做数组下标的吗?Java 行吗?C+行吗?JavaScript 就行!看看下面的语句: var arr = 1, 2, 3;alert(arr1 = arr“1“);arr“js“ = 4;alert(arr“js“);复制代码上面的语句看到,arr1和 arr“1“实际是一样的效果!这是怎么回事呢
13、?我们用下面的语句验证一下: alert(1 = “1“); / truealert(1 = “1“); / false复制代码由于 JavaScript 是弱类型语言,所以在使用变量的时候,JavaScript 会尽可能的将它转换成所需要的类型。比如数组下面需要数字,那么提供一个字符串,将会试图把字符串转换成数字。这里的“1“就成功的转换成了数字 1,于是这个语句就成立了。这就是使用 = 操作符返回 true 的原因。而 = 操作符不允许这样的类型转换,所以会返回 false。那么,这个 arr“js“怎么也能成立呢?这就不是上面的问题了。也就是说,JavaScript 实际是允许将字符串作
14、为数字下标的。这在 JavaScript 中是完全合法的。对象1. 对象对象是面向对象程序设计的基础概念之一,只需看看这个名字就已经知道了。在我们熟悉的面向对象语言中,比如 Java 或者 C+,都有着类似的对象定义方法。比如,我们想定义一个类,名字叫 Person,有两个属性: name 和 age,另外有一个方法,将显示出这个Person 对象的名字和年龄,那么我们可以用下面的代码实现:Java: public class Person private String name; private int age; public String getName() return name; pu
15、blic void setName(String name) this.name = name; public int getAge() return age; public void setAge(int age) this.age = age; public void introduction() System.out.println(“My name is “ + this.name + “, my age is “ + this.age); public static void main(String args) Person p = new Person();p.setName(“T
16、om“);p.setAge(20);p.introduction();复制代码C+的实现也是类似的,这里不再赘述。我们先来看一下这个类的定义:首先声明属性,然后定义属性的 getter 和 setter 方法,用来外界访问私有变量,最后定义了它自己的方法。这是一个比较常见的定义方式,以至于以后的很多语言,比如 C#,都采用这种定义。那么,什么是对象呢?对象不过是具有特定属性和方法的集合。虽然这并不是一个严格的定义,但是将属性和它的名字(不妨我们把它的方法也看作是它的属性,这并没有什么不同) 放在一起,形成一个集合,这就是对象。也就是说,简单来看,对象就是这种具有“键-值” 对的形式。2. Ja
17、vaScript 的对象“键-值”对的形式这个样子看上去是不是有些面熟?Bingo!对了!这不就是数组的形式吗?嗯,恭喜你想到这一点!的确,在 JavaScript 中,对象的定义就是像数组的定义。下面,我们在 JavaScript 中对这个 Person 进行一下定义: var Person = “name“: “Tom“, “age“: 20, “introduction“: function() alert(“My name is “ + this.name + “, my age is “ + this.age); ; Person.introduction();复制代码来看一下这段代
18、码。看上去很像数组的定义,只不过数组一般使用数字类型作为下标,而这里我们使用的是字符串。回想一下,其实在 JavaScript 中,字符串也是可以作为数组下标的,不是吗?好了,这里我们声明了一个对象 Person,它有一个 name,还有一个 age,而且还有一个方法显示出这两个属性。在 JavaScript 中,对象就是“ 键 -值” 对的形式,具体来说是“string-as-key“: object-as-value 的形式。也就是说,这个键必须是 string 类型的,而值可以是任何类型的。那么,方法呢?其实,JavaScript 中的 function 也是一个类型,这个在后面会有描述
19、的,这里仅仅先知道就可以了。这种数学上成为二元组的样式很常见,数组就是这样的,只不过数组的键必须是int。同样,JavaScript 的对象也是一个特殊的二元组,只不过键是 string 类型的。这是不是就像是一种散列?或者说是哈希表?就是这个样子!如果说你觉得每个属性名都要加一个引号觉得很别扭,那么你大可不加!像下面的语句,JavaScript 完全认为你的正确的: var Person = name: “Tom“,age: 20,introduction: function() alert(“My name is “ + this.name + “, my age is “ + this.
20、age);Person.introduction();复制代码我比较习惯于这种写法,看上去和 Java 等语言差不多。3. 属性的使用JavaScript 中属性的使用或许比较特别。看下面试图使用 Person 的 name 属性的四个语句,看上去都差不多,实际上也确实如此: alert(Person.name);/ alert(Person.“name“);alert(Person“name“);alert(Personname);复制代码除去注释掉的一句,其他的三个语句都能够通过解释(由于 JavaScript 是解释型语言,不是编译型的,因此这里不说是编译),但是只有 1、 3 句能够取
21、出 name 属性!第一句和 Java没有什么区别,后面的两个显得比较特别,第三句看上去像什么?对了!数组元素的访问!这进一步验证了 JavaScript 中的数组和对象“本是同根生” 。那么,第四句呢?当然是返回undefined!因为数组下标必须是数字或者字符串嘛!熟悉 Java 的话或许会对 introduction 函数有些疑问。Java 程序员不会时时刻刻把 this 加上,除非哪天心血来潮,才会加上这个 this。但是,如果你想在 JavaScript 中偷懒,去掉 this,结果只会是报错。这是怎么回事呢?简单来说,在这里的 this 关键字是必不可少的!这是JavaScript
22、 与其他语言的不同。具体为什么,会在以后的文章中说明,现在只是知道就好了不要在这里偷懒哦4. 更多属性的操作现在对 JavaScript 对象属性的认识应该在这样一点上:JavaScript 的对象就是一个二元组,或者说就是一个散列或哈希表。如果能明白这一点,就不会对下面的操作有所奇怪了: var Person = ; / 创建一个空对象Person.name = “Tom“; / 添加一个属性 name,并赋值为 TomPerson“age“ = 20; / 用另外的办法新增属性Person.introduction = function () alert(“My name is “ + t
23、his.name + “, my age is “ + this.age);Person.introduction();for(var field in Person) / 使用 foreach 循环列出对象中所有属性alert(“field name: “ + field + “; value: “ + Personfield);delete Person.name; / 删除 name 属性Person.introduction();alert(name in Person); / 使用 in 操作符判断属性是否存在复制代码5. 对象的 constructor 属性在 JavaScript
24、 中,每个对象都有一个 constructor 属性。这个 constructor 属性用来记录对象初始化时的构造函数名字。例如: var date = new Date();alert(date.constructor);alert(date.constructor = “Date“); / falsealert(date.constructor = Date); / true复制代码嗯,这个 Date 是 JavaScript 的内置对象。这里的代码看上去很平常。不过,如果你要使用自己写的对象,比如前面的 Person,就会发现它的 constructor 对象怎么是 Object?这里有
25、两个问题:第一,我们并没有给 Person constructor 属性,它怎么会有的?第二,这个constructor 属性怎么是 object,而不是我们的 Person 呢?对于第一个问题,很明显,是 JavaScript 给我们加上的。事实上,每个 JavaScript 对象都会有这样一个属性。那么,它的值又怎么是 Object 呢?这个问题,在我们说道 new 这个运算符的时候会给大家说明的。这里请大家注意,本文中的对象其实是指的单独的使用 new 之后得到的对象。也就是说,那个 constructor 属性是在 new 运算符的时候获得的。这就涉及到构造函数了不过这不是本文的重点,
26、以后再说吧 :-)函数在很多语言中,函数(Java 里面成为方法 )和对象时截然不同的两种东西。函数被定义为对象的动作,或者是全局的(像在 C+中的 main 函数一样)。但是在 JavaScript 中,函数和对象的界限却显得不那么明显。1. 函数的定义JavaScript 中有很多种定义函数的方法: function hello() alert(“Hello!“); var hello1 = function() alert(“Hello!“); ; var hello2 = new Function(“, “alert(Hello!);“); hello(); hello1(); hel
27、lo2();复制代码上面给出了三种 JavaScript 的函数定义语句。第一句是常见的定义,看上去和 Java 等语言没有太大的不同。这句是定义了一个具名函数,按照上面的例子,这里的函数定义名字为hello。第二句是将一个匿名函数定义好后赋值给一个变量,于是通过这个变量就可以引用这个匿名函数。这两句看上去效果差不多,但是它们是不一样的:第一句定义的是一个具名函数,第二句定义的是一个匿名函数尽管你可以通过这个变量引用到这个匿名函数,但实际上它还是匿名的。它们的区别可以由下面的看出: hello();hello1(); / errorfunction hello() alert(“Hello!“
28、); var hello1 = function() alert(“Hello!“); ;复制代码具名函数的作用范围是全局的:你可以在定义之前使用这个函数。但是匿名函数的定义是后向的,像 C/C+一样,必须在定义之后才能使用。这就是为什么 hello 可以使用,但是hello1 就会有错误。然后试想一下这是为什么呢?JavaScript 的解释过程和 HTML 一样是从上到下的。所以,这里的匿名函数就相当于是一个变量的定义,因此在 JavaScript 解释器解释执行时并不知道这个变量的定义,因此发生错误。但是,对于函数的定义则是扫描全局。第三个语句就很有意思了。它创建了一个 Function
29、 类的对象。这个构造函数 (姑且这么叫吧)具有两个参数,第一个是函数的参数,第二个是函数体。具体来说,下面的两个函数定义是等价的: function sayHelloTo(name) alert(“Hello, “ + name);var sayHelloTo1 = new Function(“name“, “alert(Hello, + name)“);复制代码这种使用 Function 进行定义的方式并不常见,但是这个语句显示的特性却很有趣:它意味着,你可以使用这种构造函数在运行时动态的构造函数!这是一般的语言没有的特性。2. 函数的参数JavaScript 的函数也是相当的灵活,不仅是它
30、的定义方式多种多样,甚至它的参数都有“奇怪”的行为。由于 JavaScript 是弱类型的语言,因此,它不能对你的函数参数类型做检测,甚至不能保证你传入的参数个数是否和函数定义一致。这就需要有一些特殊的检测。 function sum2(a, b) alert(a + b);sum2(1); / NaNsum2(1, 2); / 3sum2(1, 3, 5); / 4复制代码看这个例子,仅仅接受两个参数的函数,在调用时可以有任意个参数!但是,它仅取用符合条件的个数,在这里也就是前两个参数。所以,当你传入一个参数时,JavaScript 试图将两个数字加起来,结果第二个参数不存在,因此返回值是
31、NaN。第三种情况,实参个数多于形参个数,此时 JavaScript 只取前两个参数相加。尽管很不正式,但是可以说,JavaScript 的函数参数是不定参数,也就是说,你可以传入任意的参数值。使用 JavaScript 函数内置的 arguments 就可以遍历所有传入的参数。比如下面的代码: function sum() var total = 0;for(var i = 0; i 键- 值对的集合。这里的对象的属性实际上都是离散的,并不像其他的语言那样绑定到一个对象上面。this 关键字指代的是属性或者函数的调用者,也就是说,谁调用这个属性或者函数指的就是谁。可以看到,这里的 this
32、和 Java 或者 C+的 this 是有所不同的,后者的 this 是指属性或者函数所在的那个对象本身。而这里 this 的作用就是将它后面跟着的属性或者对象绑定到调用者上面。回忆一下 JavaScript 的 new 的过程,首先将创建一个空的对象,然后使用构造函数初始化这个对象,最后返回这个对象。在这个过程中,JavaScript 将把 this 用这个对象替换,也就是把对象和这些属性或函数相关联,看上去就像是这个调用者拥有这个属性或者函数似的,其实这是 this 的作用。这样看来,show 里面的 name 和 age 并没有关键字,但也是可以正常的执行就会明白怎么回事了因为前面已经用
33、 this 把 name 和 age 与这个对象 bill 相关联,并且,show 也关联到这个 bill 变量,因此 JavaScript 是可以找到这两个变量的。似乎由 this 修饰的都是公有变量。事实确实如此,如果你要使一个变量成为公有变量,可以使用 this。像上面代码中的 name 和 age 都是公有变量,在外面使用 aPerson.name 或者aPerson.age 就可以访问到。2. 私有变量怎么声明一个私有变量呢?事实上就像前面说的,JavaScript 根本没有私有作用域这一说。那么来看下面的代码: function Person(name, age) var name
34、 = name; / 私有属性var age = age;var show = function() / 私有函数alert(“name: “ + name + “; age: “ + age);var bill = new Person(“Bill“, 20);alert(bill.name); / undefinedbill.show(); / error, 不存在复制代码这段代码和前面几乎是相同的,只是把属性前面的 this 换成了 var。我们知道,var 是用来声明变量的。show 函数和 bill.name 都是未定义!这是怎么回事呢?回忆一下前面说过的 JavaScript 的
35、new 的过程。由于 name 和 age 都是使用 var 声明的,JavaScript 会将它看作是一个普通的变量,这样在构造初始化结束之后,构造函数就返回了,变量因超出作用域而访问不到。也就是说,我们使用 JavaScript 变量作用域模拟了私有属性。3. 静态变量静态变量是绑定到类上面的。对于不同的对象来说,它们共享一个静态变量。 Person.num = 0; / 静态属性function Person() this.show = function() alert(“num: “ + Person.num);Person.num+;var bill = new Person();b
36、ill.show(); / 1var tom = new Person();tom.show(); / 2bill.show(); / 2复制代码在 JavaScript 中可以很方便的添加静态属性,因为 JavaScript 的对象就是散列,所以只要简单的在类名后添加一个属性或者函数即可。4. 访问私有变量和公有变量当对私有变量进行访问时,只需要使用变量的名字就可以了,但是,如果要访问公有变量,则需要使用 this 关键字。 function Person(name, age) this.myName = name;var myAge = age;this.show = function()
37、 alert(“show = name: “ + this.myName + “; age: “ + myAge);var showAll = function() alert(“showAll = name: “ + this.myName + “; age: “ + myAge);var bill = new Person(“Bill“, 20);bill.show();复制代码在这里,如果去掉 myName 的 this 关键字,就会有未定义属性的错误。简单来说,我们需要使用 this 来声明公有变量,使用 var 来声明私有变量。但是,JavaScript 却不是那么简单,因为 Jav
38、aScript 是一个脚本语言,我们需要十分关心它的执行效率。下面,我们将会看一下 JavaScript 面向对象设计的最佳实践。闭包闭包这个概念看上去很深奥,这个词在离散数学里面的意思确实比较难于理解。在这里,我们先可以把闭包理解成是一种匿名函数或者匿名类。1. 什么是闭包?什么是闭包?一种正式的解释是:所谓闭包,指的是一种拥有很多变量并且绑定了这些变量的环境的表达式(通常是一个函数 ),因而这些变量也是这个表达式的一部分。相信很多人都不会理解这个定义,因为他的学术味道太浓了或许你喜欢从字面的语法上进行分析:首先,它是一个表达式,这个表达式绑定了很多变量以及这些变量的环境。不过这并没有什么意
39、义,这依然不会告诉我们什么是闭包。那么,来看一个例子: function add(a) return function(b) return a + b;var func = add(10);alert(func(20);复制代码我想经过了前面有关函数的描述,这个例子应该很清楚的理解。JavaScript 里面的函数就是对象,他可以做对象能做的一切事情我们首先定义了一个函数 add,它接受一个参数,这个函数返回一个匿名函数,这个匿名函数也接受一个参数,并且会返回这个参数同外部函数的那个参数的和。因此在我们使用的时候,我们将 add 返回的匿名函数赋值给 func,然后调用 func,就返回了这两
40、个数的和。当我们创建一个这样的函数,这个函数内部的一个变量能够在函数外面被引用时,我们就称创建了一个闭包。仔细的品味一下:这就是那个闭包的定义。看看我们的代码:首先,它有一个内部变量,就是那个匿名函数;其次,这个函数将匿名函数返回了出去,以便外面的变量可以引用到内部定义的变量。2. 闭包的作用闭包有什么用呢?或许现在还看不出来,那么看看这段代码: function inc(a) var i = 0;return function() return i;var num = inc();alert(num();复制代码本来,这个变量 i 在函数外面是访问不到的,因为它是 var 定义的,一旦跳出作
41、用域,这个变量就被垃圾回收了,但是,由于我们使用了闭包,在外面是能够访问到这个变量的,因此它并不被垃圾回收!如果还是不明白闭包的作用,那么看一段应该很熟悉的代码: function Person() var id;this.getId = function() return id;this.setId = function(newId) id = newId;var p = new Person();p.setId(1000);alert(p.getId(); / 1000alert(p.id); / undefined复制代码我们定义一个类 Person,它有一个 id 属性。现在这个属性的行为很像是私有变量只能通过 setter 和 getter 函数访问到。没错,这就是闭包的一个用途:制造类的私有变量!闭包还有一个作用:在内存中维护一个变量,不让垃圾回收器回收这个变量。这里的例子就不再举出了。这里我们只是简单的说了 JavaScript 的闭包的概念,并没有涉及闭包的内存模型等等之类。这是一个相当重要的概念,Java 社区中的部分成员一直对闭包梦寐以求, C#也已经在最新版本中添加了闭包的概念,只不过在那里称为 lambda 表达式。