导航

JavaScript面向对象程序设计2(转载)

Posted on 2009-07-21 11:38  鸡尾虾的壳  阅读(255)  评论(0编辑  收藏  举报
From: 豆子空间,http://jbean.javaeye.com/blog/406265

类是面向对象程序设计的核心概念之一。一个类代表了具有相似属性的一类事物的抽象。从本篇开始,我们将正式的进入JavaScript的面向对象部分。首先需要注意的是,在JavaScript中并没有“类”这一关键字——在大多数语言中都是使用class作为关键字的。所以,这里的类就成了一个概念,它没有明确的语法标志。

 

1. 类和构造函数

 

前面说过,在JavaScript中并没有明确的类的概念。实际上,我们给出的只是类的构造函数。类的构造函数构成了这个类的全部内容。既然叫做构造函数,它也是一个普通的函数,没有什么不同之处。因此,我们能够很轻易的定义出一个构造函数:

 

Js代码 复制代码
  1. function Person(name, age) {   
  2.     this.name = name;   
  3.     this.age = age;   
  4.     this.show = function() {   
  5.         alert("Hello, my name is " + this.name + ", my age is " + this.age);   
  6.     };   
  7. }  

 

这里,我们定义了一个类 Person,它有两个属性:name和age;有一个方法:show。看上去和其他语言的类的定义没有什么不同。其实,这里最大的不同就是在于这个关键字function。我们使用了定义函数的方式定义了一个类。

 

2. new

 

定义出来类之后,需要创建类的对象。同其他语言一眼,JavaScript也使用new操作符创建对象。具体代码如下:

 

 

Js代码 复制代码
  1. var bill = new Person("Bill", 30);   
  2. alert(bill.name);   
  3. alert(bill["age"]);   
  4. bill.show();  

 

这里使用new创建一个Person类的对象。和其他语言类似,new之后是该类的构造函数。当创建对象之后,就可以像前面的章节中说到的一样,使用.或者[]对属性进行访问。

 

注意一下,这里的构造函数就是一个普通的函数,那么,是不是所有的函数都可以使用new操作符呢?答案是肯定的。那么,这个new操作符到底做了什么呢?

 

当使用new操作符的时候,首先JavaScript会创建一个空的对象,然后将会对这个对象进行初始化。用什么来初始化呢?当然就是你调用的那个构造函数了。最后,这个创建的对象将返回给调用者,于是,我们就可以使用这个对象了。

 

3. prototype

 

prototype是原型的意思。在JavaScript中,每个对象都有一个prototype属性。这个属性指向一个prototype对象。这就是原型属性和原型对象的概念。

 

每个对象都有一个prototype属性,构造函数是一个普通的函数,而函数也是一个对象,因此,构造函数也有一个prototype属性。而每个prototype对象都有一个constructor属性,这个prototype对象的constructor属性指向这个prototype属性所在的构造函数本身。也就是说,new操作符要保证生成的对象的prototype属性和构造函数的prototype属性是一致的。

 

有点迷糊了不是?看一下附件中的图,无论怎样,都要保证这个图所示的关系式正确的!

 

需要大家注意的是,这个prototype对象是JavaScript的面向对象的基础,包括继承等的实现都是使用prototype。

 

4. 一个实现技巧:检测参数非空和设置参数默认值

 

由于JavaScript函数对于参数控制比较困难,因此参数检测成为一个不可忽视的问题。这里给出一个编程的小技巧,能够检查传入的实参是否非空,以及给参数设置默认值。

 

Js代码 复制代码
  1. function print(mustHave, person) {   
  2.     var defaultPerson = {   
  3.         name: "noname",   
  4.         age: 0   
  5.     };   
  6.     if(!mustHave) { // 非空检测   
  7.         alert("mustHave should not be null!");   
  8.         return;   
  9.     }   
  10.     person = person || defaultPerson; // 设置默认值   
  11.     alert(mustHave + ": name- " + person.name + "; age- " + person.age);   
  12. }   
  13. print();   
  14. print("sth");   
  15. print("sth", {name: "new", age: 20});  

 

非空检测比较简单。默认值的设置比较有技巧,利用了JavaScript的||操作的短路特性。如果形参person为空,那么||前半部分为false,通过或操作,将把person设置为defaultPerson;如果person非空,则||直接返回true,那么就不进行或操作。


封装是面向对象的重要概念之一。如果一个程序没有封装性,也就谈不上什么面向对象。但是,JavaScript并不像其他的语言,比如Java,有公有变量和私有变量等;在JavaScript中只有一种作用域:公有作用域。在本章中,我们将会见识到JavaScript是如何实现封装的特性的。

 

1. this和公有变量

 

首先需要理解this关键字。看下面的一段代码,你应该对此感到熟悉:

 

Js代码 复制代码
  1. function Person(name, age) {   
  2.     this.name = name; // 定义一个公有变量   
  3.    this.age = age;   
  4.     this.show = function() { // 定义一个公有函数   
  5.         alert("name: " + name + "; age: " + age);   
  6.     }   
  7. }   
  8. var bill = new Person("Bill", 20);   
  9. alert(bill.name);   
  10. bill.show();  

 

这里的this关键字是必不可少的。前面只是让大家记住,那么为什么要这样呢?想想JavaScript的对象,JavaScript的对象类似于散列,一个<string, object>键-值对的集合。这里的对象的属性实际上都是离散的,并不像其他的语言那样绑定到一个对象上面。this关键字指代的是属性或者函数的调用者,也就是说,谁调用这个属性或者函数指的就是谁。可以看到,这里的this和Java或者C++的this是有所不同的,后者的this是指属性或者函数所在的那个对象本身。而这里this的作用就是将它后面跟着的属性或者对象绑定到调用者上面。回忆一下JavaScript的new的过程,首先将创建一个空的对象,然后使用构造函数初始化这个对象,最后返回这个对象。在这个过程中,JavaScript将把this用这个对象替换,也就是把对象和这些属性或函数相关联,看上去就像是这个调用者拥有这个属性或者函数似的,其实这是this的作用。

 

这样看来,show里面的name和age并没有关键字,但也是可以正常的执行就会明白怎么回事了——因为前面已经用this把name和age与这个对象bill相关联,并且,show也关联到这个bill变量,因此JavaScript是可以找到这两个变量的。

 

这样来看,似乎由this修饰的都是公有变量。事实确实如此,如果你要使一个变量成为公有变量,可以使用this。像上面代码中的name和age都是公有变量,在外面使用aPerson.name或者aPerson.age就可以访问到。

 

2. 私有变量

 

怎么声明一个私有变量呢?事实上就像前面说的,JavaScript根本没有私有作用域这一说。那么来看下面的代码:

 

Js代码 复制代码
  1. function Person(name, age) {   
  2.     var name = name; // 私有属性   
  3.    var age = age;   
  4.     var show = function() { // 私有函数   
  5.         alert("name: " + name + "; age: " + age);   
  6.     }   
  7. }   
  8. var bill = new Person("Bill", 20);   
  9. alert(bill.name); // undefined   
  10. bill.show(); // error, 不存在  

  

这段代码和前面几乎是相同的,只是把属性前面的this换成了var。我们知道,var是用来声明变量的。show函数和bill.name都是未定义!这是怎么回事呢?

 

回忆一下前面说过的JavaScript的new的过程。由于name和age都是使用var声明的,JavaScript会将它看作是一个普通的变量,这样在构造初始化结束之后,构造函数就返回了,变量因超出作用域而访问不到。也就是说,我们使用JavaScript变量作用域模拟了私有属性。

 

3. 静态变量

 

静态变量是绑定到类上面的。对于不同的对象来说,它们共享一个静态变量。

 

Js代码 复制代码
  1. Person.num = 0; // 静态属性   
  2. function Person() {   
  3.     this.show = function() {   
  4.         alert("num: " + Person.num);   
  5.     };   
  6.     Person.num++;   
  7. }   
  8. var bill = new Person();   
  9. bill.show(); // 1   
  10. var tom = new Person();   
  11. tom.show(); // 2   
  12. bill.show(); // 2  

 

在JavaScript中可以很方便的添加静态属性,因为JavaScript的对象就是散列,所以只要简单的在类名后添加一个属性或者函数即可。

 

4. 访问私有变量和公有变量

 

当对私有变量进行访问时,只需要使用变量的名字就可以了,但是,如果要访问公有变量,则需要使用this关键字。

 

Js代码 复制代码
  1. function Person(name, age) {   
  2.     this.myName = name;   
  3.     var myAge = age;   
  4.     this.show = function() {   
  5.         alert("show = name: " + this.myName + "; age: " + myAge);   
  6.     }   
  7.     var showAll = function() {   
  8.         alert("showAll = name: " + this.myName + "; age: " + myAge);   
  9.     }   
  10. }   
  11. var bill = new Person("Bill", 20);   
  12. bill.show();  

 

在这里,如果去掉myName的this关键字,就会有未定义属性的错误。

 

简单来说,我们需要使用this来声明公有变量,使用var来声明私有变量。但是,JavaScript却不是那么简单,因为JavaScript是一个脚本语言,我们需要十分关心它的执行效率。下面,我们将会看一下JavaScript面向对象设计的最佳实践。

闭包这个概念看上去很深奥,这个词在离散数学里面的意思确实比较难于理解。在这里,我们先可以把闭包理解成是一种匿名函数或者匿名类。

 

1. 什么是闭包?

 

什么是闭包?一种正式的解释是:所谓闭包,指的是一种拥有很多变量并且绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是这个表达式的一部分。

 

相信很多人都不会理解这个定义,因为他的学术味道太浓了——或许你喜欢从字面的语法上进行分析:首先,它是一个表达式,这个表达式绑定了很多变量以及这些变量的环境。不过这并没有什么意义,这依然不会告诉我们什么是闭包。

 

那么,来看一个例子:

 

Js代码 复制代码
  1. function add(a) {   
  2.     return function(b) {   
  3.         return a + b;   
  4.     };   
  5. }   
  6. var func = add(10);   
  7. alert(func(20));  

 

我想经过了前面有关函数的描述,这个例子应该很清楚的理解。JavaScript里面的函数就是对象,他可以做对象能做的一切事情——我们首先定义了一个函数add,它接受一个参数,这个函数返回一个匿名函数,这个匿名函数也接受一个参数,并且会返回这个参数同外部函数的那个参数的和。因此在我们使用的时候,我们将add返回的匿名函数赋值给func,然后调用func,就返回了这两个数的和。

 

当我们创建一个这样的函数,这个函数内部的一个变量能够在函数外面被引用时,我们就称创建了一个闭包。仔细的品味一下:这就是那个闭包的定义。

 

看看我们的代码:首先,它有一个内部变量,就是那个匿名函数;其次,这个函数将匿名函数返回了出去,以便外面的变量可以引用到内部定义的变量。

 

2. 闭包的作用

 

闭包有什么用呢?或许现在还看不出来,那么看看这段代码:

 

Js代码 复制代码
  1. function inc(a) {   
  2.     var i = 0;   
  3.     return function() {   
  4.         return i;   
  5.     };   
  6. }   
  7. var num = inc();   
  8. alert(num());  

 

本来,这个变量 i 在函数外面是访问不到的,因为它是 var 定义的,一旦跳出作用域,这个变量就被垃圾回收了,但是,由于我们使用了闭包,在外面是能够访问到这个变量的,因此它并不被垃圾回收!

 

如果还是不明白闭包的作用,那么看一段应该很熟悉的代码:

 

Js代码 复制代码
  1. function Person() {   
  2.     var id;   
  3.     this.getId = function() {   
  4.         return id;   
  5.     }   
  6.     this.setId = function(newId) {   
  7.         id = newId;   
  8.     }   
  9. }   
  10. var p = new Person();   
  11. p.setId(1000);   
  12. alert(p.getId()); // 1000   
  13. alert(p.id); // undefined  

 

我们定义一个类Person,它有一个id属性。现在这个属性的行为很像是私有变量——只能通过 setter 和 getter 函数访问到。没错,这就是闭包的一个用途:制造类的私有变量!

 

闭包还有一个作用:在内存中维护一个变量,不让垃圾回收器回收这个变量。这里的例子就不再举出了。

 

这里我们只是简单的说了JavaScript的闭包的概念,并没有涉及闭包的内存模型等等之类。这是一个相当重要的概念,Java社区中的部分成员一直对闭包梦寐以求,C#也已经在最新版本中添加了闭包的概念,只不过在那里称为lambda表达式。

优雅的封装还是执行的效率?这是一个悖论。

 

优雅封装的程序看起来是那么的美妙:每个属性被隐藏在对象之后,你所能看到的就是这个对象让你看到的,至于它到底是怎么操作的,这个不需要你操心。

 

执行的效率就是另外一回事。就像是C语言和面向对象的C++之间的差别:C++很优雅,但是执行效率,无论是编译后的二进制代码还是运行期的内存的占用,都要比简单的C语言多出一截来。

 

这个问题在脚本语言中显得更加重要,因为JavaScript根本就是一种解释语言,解释语言的执行效率要比编译语言低很多。

 

1. 优雅的封装

 

我们先来看看变量封装。这里的变量不仅仅是属性,也包括函数。

 

前面已经说过,JavaScript中并没有类这个概念,是我们利用变量作用域和闭包“巧妙的模拟”出来的,这是一种优雅的实现。还是温故一下以前的代码:

 

Js代码 复制代码
  1. function Person() {   
  2.     var id;   
  3.     var showId = function() {   
  4.         alert("My id is " + id);   
  5.     }   
  6.     this.getId = function() {   
  7.         return id;   
  8.     }   
  9.     this.setId = function(newId) {   
  10.         id = newId;   
  11.     }   
  12. }   
  13. var p = new Person();   
  14. p.setId(1000);   
  15. alert(p.id); // undefined   
  16. // p.showId(); error: function not defined   
  17. var p2 = new Person();   
  18. alert(p.getId == p2.getId); // false  

 

我们很优雅的实现了私有变量——尽管是投机取巧的实现的。但是,这段代码又有什么问题呢?为什么两个对象的函数是不同的呢?

 

想一下,我们使用变量的作用域模拟出私有变量,用闭包模拟出公有变量,那么,也就是说,实际上每个创建的对象都会有一个相同的代码的拷贝!不仅仅是那个id,就连那些showId、getId 等函数也会创建多次。注意,考虑到JavaScript函数就是对象,就不会感到那么奇怪了。但是毫无疑问,这是一种浪费:每个变量所不同的只是自己的数据域,函数代码都是相同的,因为我们进行的是同一种操作。其他语言一般不会遇到这种问题,因为那些语言的函数和对象的概念是不同的,像Java,每个对象的方法其实指向了同一份代码的拷贝,而不是每个对象都会有自己的代码拷贝。

 

2. 去看效率

 

那种封装虽然优雅,但是很浪费。好在JavaScript是一种灵活的语言,于是,我们马上想到,把这些函数的指针指向另外的一个函数不就可以了吗?

 

Js代码 复制代码
  1. function show() {   
  2.     alert("I'm a person.");   
  3. }   
  4. function Person() {   
  5.     this.show = show;   
  6. }   
  7. var p1 = new Person();   
  8. var p2 = new Person();   
  9. alert(p1.show == p2.show); // true  

 

这个办法不错,解决了我们以前的那个问题:不同的对象共享了一份代码。但是这种实现虽然有了效率,可是却太不优雅了——如果我有很多类,那么岂不是有很多全局函数?

 

好在JavaScript中还有一个机制:prototype。还记得这个prototype吗?每个对象都维护着一个prototype属性,这些对象的prototype属性是共享的。那么,我们就可以把函数的定义放到prototype里面,于是,不同的对象不就共享了一份代码拷贝吗?事实确实如此:

 

Js代码 复制代码
  1. function Person() {   
  2. }   
  3. Person.prototype.show = function() {   
  4.     alert("I'm a person.");   
  5. }   
  6. var p1 = new Person();   
  7. var p2 = new Person();   
  8. alert(p1.show == p2.show); // true  

 

不过,这种分开定义看上去很别扭,那么好,为什么不把函数定义也写到类定义里面呢?

 

Js代码 复制代码
  1. function Person() {      
  2.     Person.prototype.show = function() {      
  3.         alert("I'm a person.");      
  4.     }      
  5. }      
  6. var p1 = new Person();      
  7. var p2 = new Person();      
  8. alert(p1.show == p2.show); // true     

 

实际上这种写法和上面一种没有什么不同:唯一的区别就是代码位置不同。这只是一个“看上去很甜”的语法糖,并没有实质性差别。

 

最初,微软的.Net AJAX框架使用前面的机制模拟了私有变量和函数,这种写法和C#很相像,十分的优雅。但是,处于效率的缘故,微软后来把它改成了这种原型的定义方式。虽然这种方式不那么优雅,但是很有效率。

 

在JavaScript中,这种封装的优雅和执行的效率之间的矛盾一直存在。现在我们最好的解决方案就是把数据定义在类里面,函数定义在类的prototype属性里面。