`

Javascript面向对象程序设计

阅读更多

======================== 1.前言 ===============================

1. 基于对象还是面向对象?

面向对象技术是现代软件开发中的重要技术之一。面向对象变成的好处毋庸置疑,现在的主流语言如Java、C++都是面向对象的。现在的面向对象理论更多的是使用Java或C++进行描述,究其根源,在于这些语言都是传统的面向对象语言,具有面向对象理论所指明的一切特性:类、封装、继承、多态等等。

相比而言,一些动态语言如JavaSript就显得不那么面向对象——至少,在JavaScript中并没有类class这一关键字。但是,在JavaScript中并不是没有类的概念。于是有人说,JavaScript是基于对象的语言,而不是面向对象的语言。

面向对象的语言具有三个特性:封装、继承和多态,三者缺一不可;基于对象的语言通常仅仅是使用对象,其实现的是封装,并没有提供后两种特性。的确,从语法上来说,JavaScript并没有特定的语法或者在语言级别上来实现继承和多态。但是,这并不妨碍我们使用这些特性。这是因为,JavaScript是一种灵活的语言,它是相当的灵活,以至于这些并没有提供的东西,更确切的说,是没有明确的表明的东西,都是可以实现和使用的!那么,你还能说JavaScript是基于对象而不是面向对象的吗?

面向对象也是一种思想,任何语言,包括C语言,同样可以使用面向对象的思想去解决现实生活中的各种问题。到底是基于对象还是面向对象,这些概念让计算机哲学家门去争论吧——相信他们的争论最终也会和先有鸡还是先有蛋的问题一样的结果——我们所要做的,是要使用这种语言提供的机制去解决我们的问题。

2. 为什么要有JavaScript的面向对象编程?

这个问题很严肃——这取决你问题的规模和应用的范围。就像JavaEE和PHP一样:PHP能实现的东西,JavaEE都能实现,那么,为什么还要有PHP?因为JavaEE太复杂了,对于一些简单的系统,根本没有必要是使用它,也就是所谓的“杀鸡焉用牛刀”。

JavaScript主要应用于Web开发中。在传统的Web开发模式中,JavaScript起到的是一些点缀的作用,只完成很有限的功能,例如表单验证等。于是,JavaScript多被当做一种过程性语言使用,很难完成复杂的功能。而今天Web2.0的时代,Ajax大行其道,很多复杂的脚本成为其必须的组成部分。在Ajax应用中利用JavaScript面向对象编程风格,能够使逻辑更加清晰,也更有利于问题的解决。

如果你想用JavaScript编写一个库,比如ExtJS或者YUI,很难想象你的类库不使用面向对象的编程风格——否则的话,无论是对你还是对使用者的智力都将是一个前所未有的考验!或许,自从面向对象思想提出之后,已经很难有类库不使用面向对象的方式实现了,即便是C语言的库诸如gtk+,也是用C语言将面向对象的思想表现的天衣无缝。面向对象的思想对于大型程序的编写和使用具有不可替代的作用。

本系列文章将试图向读者阐述JavaScript的面向对象程序设计。尽管JavaScript中具有很多浏览器相关的概念,如document等内置对象,但是本系列将不涉及这些问题,并且将假设读者已经有JavaScript基础的语法知识等。本系列文章不会从头开始讲述JavaScript的语法,仅仅从纯粹的面向对象角度审视JavaScript,或许,你将会看到一个教程:面向对象程序设计——JavaScript语言描述。这才是本系列文章的目的。

======================== 2.数组 ===============================

或许你会奇怪,面向对象的程序设计为什么从数组开始讲起?这是因为……其间的种种关系吧……嘿嘿,这里先卖个关子,先来看看我们熟悉的数组在JavaScript里面是什么样子的。

1. 创建数组

在JavaScript中有很多创建数组的方法。比如使用Array函数。不过这不是现在我们要讲述的。现在我们使用简单的方括号“[]”的办法来创建数组。

Js代码
1.var objAyyar = []; // 1   
2.var objAyyar = [2]; // 2   
3.var objAyyar = ["a", "b", "c"]; // 3   
4.var objAyyar = [new Date(), 123, "abc"]; // 4 
var objAyyar = []; // 1
var objAyyar = [2]; // 2
var objAyyar = ["a", "b", "c"]; // 3
var objAyyar = [new Date(), 123, "abc"]; // 4
这里有四个创建数组的语句。下面来一一解释一下:

第一句,创建一个空的数组;

第二句,创建一个数组,数组元素只有一个2;

第三句,创建一个数组,数组的元素分别初始化为"a", "b", "c";

第四句,创建一个数组,其中第一个元素为一个Date类型的对象,第二个元素是数字123,第三个元素是字符串"abc"。

回顾一下,在Java或者C++语言中,数组是具有相同的数据类型的元素的集合。比如使用Java语言的下面语句

Java代码
1.int[] array = new int[10]; 
int[] array = new int[10];
将创建一个能放入10个int类型的元素的数组。数组和其他类型的集合的一个很大的区别是,数组里面只能存放相同数据类型的元素(使用泛型的集合除外)。但是,像上面的第四句,JavaScript的数组怎么能存放不同类型的元素呢?这是因为,JavaScript是弱类型的语言,没有很大的数据类型的差别,所以数组的元素可以放入不同的类型。

2. 操作数组

数组是元素的有序集合。数组中的元素是有序的,这就可以通过下标访问到数组中的每个元素。而且,JavaScript的数组相当的灵活。当你习惯了Java或者C++的数组之后,或许并不习惯JavaScript的数组。在一定程度上,这种数组可以称为一种动态数组。看这样一段代码:

Js代码
1.var arr = [1, 2, 3, 4, 5];      
2.alert(arr.length); // 数组长度为5   
3.alert(arr[3]); // arr[3] = 4   
4.arr[9] = 10;    // 改变了数组的长度为10   
5.alert(arr[7]);   
6.alert(arr.length); 
var arr = [1, 2, 3, 4, 5];   
alert(arr.length); // 数组长度为5
alert(arr[3]); // arr[3] = 4
arr[9] = 10;    // 改变了数组的长度为10
alert(arr[7]);
alert(arr.length); 
首先创建一个数组arr,可以看到它的长度是5,arr[3]是4。这些都是很常见的。那么第三句,arr[9] = 10;就有点意思了——在Java中,这句操作将导致数组越界的异常,在C++中,这种操作是极其危险的。但是在JavaScript中,这样的操作是正常的——你可以动态的改变数组的大小!虽然你在创建数组时并没有这么大的长度,但是,你可以在创建之后指定它!这时的arr.length已经自动的变成10了。那么,arr[7]又会是什么呢?经过运行代码我们会看到,arr[7]是undefined。也就是说,虽然arr[9]有了值,但是其中从arr[5]到arr[8]这几个元素都是未定义的,也就是undefined。如果你问JavaScript怎么不给个初始值?唉,饶了它吧!JavaScript并不知道你想要它初始化成什么值啊!万一错了呢?干脆还是别了吧……

Js代码
1.var arr = [1, 2, 3, 4, 5];           
2.alert(arr.length); // 数组长度为5   
3.delete arr[3]; // 删掉第4个元素   
4.alert(arr.length); // 长度不变   
5.alert(arr[3]); // arr[3] = undefined   
6.arr.length = 4; // 缩短长度   
7.alert(arr[4]);   
8.arr.length = 10; // 增加长度   
9.alert(arr[6]); 
var arr = [1, 2, 3, 4, 5];        
alert(arr.length); // 数组长度为5
delete arr[3]; // 删掉第4个元素
alert(arr.length); // 长度不变
alert(arr[3]); // arr[3] = undefined
arr.length = 4; // 缩短长度
alert(arr[4]);
arr.length = 10; // 增加长度
alert(arr[6]); 
上面的代码也很有意思:使用delete操作符可以删除任意一个数组元素,但是长度并不改变。

Java的数组也有一个length属性,用来显示数组的长度。JavaScript的数组也有这个属性。但是,和Java不同的是,后者的length属性并不是只读的!你可以任意的设置数组的length属性的值,无论是扩大还是缩小!只是如上面的代码所示,改变了length之后,越界的元素或者以前没有定义的元素都将成为undefined。也就是说,当length大于原始长度时,从原长度到length - 1的元素都将成为undefined;当length小于原始长度时,从length到原长度 - 1的元素也都会清除设置为undefined。

3. 非数字的下标?

如果动态的length属性还不够灵活的话,那么,JavaScript的数组还有另外的能力。

你见到过用字符串做数组下标的吗?Java行吗?C++行吗?JavaScript就行!看看下面的语句:

Js代码
1.var arr = [1, 2, 3];   
2.alert(arr[1] == arr["1"]);   
3.arr["js"] = 4;   
4.alert(arr["js"]); 
var arr = [1, 2, 3];
alert(arr[1] == arr["1"]);
arr["js"] = 4;
alert(arr["js"]); 
上面的语句看到,arr[1]和arr["1"]实际是一样的效果!这是怎么回事呢?我们用下面的语句验证一下:

Js代码
1.alert(1 == "1"); // true   
2.alert(1 === "1"); // false 
alert(1 == "1"); // true
alert(1 === "1"); // false 
由于JavaScript是弱类型语言,所以在使用变量的时候,JavaScript会尽可能的将它转换成所需要的类型。比如数组下面需要数字,那么提供一个字符串,将会试图把字符串转换成数字。这里的"1"就成功的转换成了数字1,于是这个语句就成立了。这就是使用 == 操作符返回true的原因。而 === 操作符不允许这样的类型转换,所以会返回false。

那么,这个arr["js"]怎么也能成立呢?这就不是上面的问题了。也就是说,JavaScript实际是允许将字符串作为数字下标的。这在JavaScript中是完全合法的。

======================== 3.对象 ===============================

1. 对象

对象是面向对象程序设计的基础概念之一,只需看看这个名字就已经知道了。在我们熟悉的面向对象语言中,比如Java或者C++,都有着类似的对象定义方法。比如,我们想定义一个类,名字叫Person,有两个属性:name和age,另外有一个方法,将显示出这个Person对象的名字和年龄,那么我们可以用下面的代码实现:

Java代码
1.public class Person {      
2.        private String name;      
3.        private int age;      
4. 
5.        public String getName() {      
6.                return name;      
7.        }      
8. 
9.        public void setName(String name) {      
10.                this.name = name;      
11.        }      
12. 
13.        public int getAge() {      
14.                return age;      
15.        }      
16. 
17.        public void setAge(int age) {      
18.                this.age = age;      
19.        }      
20. 
21.        public void introduction() {      
22.                System.out.println("My name is " + this.name + ", my age is " + this.age);      
23.        }   
24. 
25.        public static void main(String[] args) {   
26.                Person p = new Person();   
27.                p.setName("Tom");   
28.                p.setAge(20);   
29.                p.introduction();   
30.        }   
31.} 
public class Person {   
        private String name;   
        private int age;   

        public String getName() {   
                return name;   
        }   

        public 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("Tom");
                p.setAge(20);
                p.introduction();
        }

C++的实现也是类似的,这里不再赘述。

我们先来看一下这个类的定义:首先声明属性,然后定义属性的getter和setter方法,用来外界访问私有变量,最后定义了它自己的方法。这是一个比较常见的定义方式,以至于以后的很多语言,比如C#,都采用这种定义。

那么,什么是对象呢?对象不过是具有特定属性和方法的集合。虽然这并不是一个严格的定义,但是将属性和它的名字(不妨我们把它的方法也看作是它的属性,这并没有什么不同)放在一起,形成一个集合,这就是对象。也就是说,简单来看,对象就是这种具有“键-值”对的形式。

2. JavaScript的对象

“键-值”对的形式……这个样子看上去是不是有些面熟?Bingo!对了!这不就是数组的形式吗?嗯,恭喜你想到这一点!的确,在JavaScript中,对象的定义就是像数组的定义。下面,我们在JavaScript中对这个Person进行一下定义:

Js代码
1.var Person = {      
2.        "name": "Tom",      
3.        "age": 20,      
4.        "introduction": function() {      
5.                alert("My name is " + this.name + ", my age is " + this.age);      
6.        }      
7.};      
8.Person.introduction(); 
var Person = {   
        "name": "Tom",   
        "age": 20,   
        "introduction": function() {   
                alert("My name is " + this.name + ", my age is " + this.age);   
        }   
};   
Person.introduction(); 
来看一下这段代码。看上去很像数组的定义,只不过数组一般使用数字类型作为下标,而这里我们使用的是字符串。回想一下,其实在JavaScript中,字符串也是可以作为数组下标的,不是吗?好了,这里我们声明了一个对象Person,它有一个name,还有一个age,而且还有一个方法显示出这两个属性。

在JavaScript中,对象就是“键-值”对的形式,具体来说是"string-as-key": object-as-value的形式。也就是说,这个键必须是string类型的,而值可以是任何类型的。那么,方法呢?其实,JavaScript中的function也是一个类型,这个在后面会有描述的,这里仅仅先知道就可以了。这种数学上成为二元组的样式很常见,数组就是这样的,只不过数组的键必须是int。同样,JavaScript的对象也是一个特殊的二元组,只不过键是string类型的。这是不是就像是一种散列?或者说是哈希表?就是这个样子!

如果说你觉得每个属性名都要加一个引号觉得很别扭,那么你大可不加!像下面的语句,JavaScript完全认为你的正确的:

Js代码
1.var Person = {   
2.        name: "Tom",   
3.        age: 20,   
4.        introduction: function() {   
5.                alert("My name is " + this.name + ", my age is " + this.age);   
6.        }   
7.}   
8.Person.introduction(); 
var Person = {
        name: "Tom",
        age: 20,
        introduction: function() {
                alert("My name is " + this.name + ", my age is " + this.age);
        }
}
Person.introduction(); 
我比较习惯于这种写法,看上去和Java等语言差不多。

3. 属性的使用

JavaScript中属性的使用或许比较特别。看下面试图使用Person的name属性的四个语句,看上去都差不多,实际上也确实如此:

Js代码
1.alert(Person.name); // Tom  
2.// alert(Person."name");   
3.alert(Person["name"]); // Tom  
4.alert(Person[name]); // undefined 
alert(Person.name); // Tom
// alert(Person."name");
alert(Person["name"]); // Tom
alert(Person[name]); // undefined 
除去注释掉的一句,其他的三个语句都能够通过解释(由于JavaScript是解释型语言,不是编译型的,因此这里不说是编译),但是只有1、3句能够取出name属性!第一句和Java没有什么区别,后面的两个显得比较特别,第三句看上去像什么?对了!数组元素的访问!这进一步验证了JavaScript中的数组和对象“本是同根生”。那么,第四句呢?当然是返回undefined!因为数组下标必须是数字或者字符串嘛!

熟悉Java的话或许会对introduction函数有些疑问。Java程序员不会时时刻刻把this加上,除非哪天心血来潮,才会加上这个this。但是,如果你想在JavaScript中偷懒,去掉this,结果只会是报错。这是怎么回事呢?简单来说,在这里的this关键字是必不可少的!这是JavaScript与其他语言的不同。具体为什么,会在以后的文章中说明,现在只是知道就好了——不要在这里偷懒哦~~

4. 更多属性的操作

现在对JavaScript对象属性的认识应该在这样一点上:JavaScript的对象就是一个二元组,或者说就是一个散列或哈希表。如果能明白这一点,就不会对下面的操作有所奇怪了:

Js代码
1.var Person = {}; // 创建一个空对象   
2.Person.name = "Tom"; // 添加一个属性name,并赋值为Tom   
3.Person["age"] = 20; // 用另外的办法新增属性   
4.Person.introduction = function () {   
5.        alert("My name is " + this.name + ", my age is " + this.age);   
6.};   
7.Person.introduction();   
8.for(var field in Person) { // 使用foreach循环列出对象中所有属性   
9.        alert("field name: " + field + "; value: " + Person[field]);   
10.}   
11.delete Person.name; // 删除name属性   
12.Person.introduction();   
13.alert(name in Person); // 使用in操作符判断属性是否存在 
var Person = {}; // 创建一个空对象
Person.name = "Tom"; // 添加一个属性name,并赋值为Tom
Person["age"] = 20; // 用另外的办法新增属性
Person.introduction = function () {
        alert("My name is " + this.name + ", my age is " + this.age);
};
Person.introduction();
for(var field in Person) { // 使用foreach循环列出对象中所有属性
        alert("field name: " + field + "; value: " + Person[field]);
}
delete Person.name; // 删除name属性
Person.introduction();
alert(name in Person); // 使用in操作符判断属性是否存在 
5. 对象的constructor属性

在JavaScript中,每个对象都有一个constructor属性。这个constructor属性用来记录对象初始化时的构造函数名字。例如:

Js代码
1.var date = new Date();   
2.alert(date.constructor);   
3.alert(date.constructor == "Date"); // false   
4.alert(date.constructor == Date); // true 
var date = new Date();
alert(date.constructor);
alert(date.constructor == "Date"); // false
alert(date.constructor == Date); // true 
嗯,这个Date是JavaScript的内置对象。这里的代码看上去很平常。不过,如果你要使用自己写的对象,比如前面的Person,就会发现它的constructor对象怎么是Object?这里有两个问题:第一,我们并没有给Person constructor属性,它怎么会有的?第二,这个constructor属性怎么是object,而不是我们的Person呢?

对于第一个问题,很明显,是JavaScript给我们加上的。事实上,每个JavaScript对象都会有这样一个属性。那么,它的值又怎么是Object呢?这个问题,在我们说道new这个运算符的时候会给大家说明的。这里请大家注意,本文中的对象其实是指的单独的使用new之后得到的对象。也就是说,那个constructor属性是在new运算符的时候获得的。这就涉及到构造函数了——不过这不是本文的重点,以后再说吧~~ :-)

======================== 4.函数 ===============================

在很多语言中,函数(Java里面称为方法)和对象时截然不同的两种东西。函数被定义为对象的动作,或者是全局的(像在C++中的main函数一样)。但是在JavaScript中,函数和对象的界限却显得不那么明显。



1. 函数的定义



JavaScript中有很多种定义函数的方法:



Js代码
1.function hello() { alert("Hello!"); }  
2.var hello1 = function() { alert("Hello!"); };  
3.var hello2 = new Function("", "alert('Hello!');");  
4.hello();  
5.hello1();  
6.hello2(); 
function hello() { alert("Hello!"); }
var hello1 = function() { alert("Hello!"); };
var hello2 = new Function("", "alert('Hello!');");
hello();
hello1();
hello2();

上面给出了三种JavaScript的函数定义语句。第一句是常见的定义,看上去和Java等语言没有太大的不同。这句是定义了一个具名函数,按照上面的例子,这里的函数定义名字为hello。第二句是将一个匿名函数定义好后赋值给一个变量,于是通过这个变量就可以引用这个匿名函数。这两句看上去效果差不多,但是它们是不一样的:第一句定义的是一个具名函数,第二句定义的是一个匿名函数——尽管你可以通过这个变量引用到这个匿名函数,但实际上它还是匿名的。它们的区别可以由下面的看出:



Js代码
1.hello();  
2.hello1(); // error  
3.function hello() { alert("Hello!"); }  
4.var hello1 = function() { alert("Hello!"); }; 
hello();
hello1(); // error
function hello() { alert("Hello!"); }
var hello1 = function() { alert("Hello!"); };

具名函数的作用范围是全局的:你可以在定义之前使用这个函数。但是匿名函数的定义是后向的,像C/C++一样,必须在定义之后才能使用。这就是为什么hello可以使用,但是hello1就会有错误。然后试想一下这是为什么呢?JavaScript的解释过程和HTML一样是从上到下的。所以,这里的匿名函数就相当于是一个变量的定义,因此在JavaScript解释器解释执行时并不知道这个变量的定义,因此发生错误。但是,对于函数的定义则是扫描全局。



第三个语句就很有意思了。它创建了一个Function类的对象。这个构造函数(姑且这么叫吧)具有两个参数,第一个是函数的参数,第二个是函数体。具体来说,下面的两个函数定义是等价的:



Js代码
1.function sayHelloTo(name) {  
2.    alert("Hello, " + name);  
3.}  
4.var sayHelloTo1 = new Function("name", "alert('Hello, ' + name)"); 
function sayHelloTo(name) {
    alert("Hello, " + name);
}
var sayHelloTo1 = new Function("name", "alert('Hello, ' + name)");

这种使用Function进行定义的方式并不常见,但是这个语句显示的特性却很有趣:它意味着,你可以使用这种构造函数在运行时动态的构造函数!这是一般的语言没有的特性。



2. 函数的参数



JavaScript的函数也是相当的灵活,不仅是它的定义方式多种多样,甚至它的参数都有“奇怪”的行为。由于JavaScript是弱类型的语言,因此,它不能对你的函数参数类型做检测,甚至不能保证你传入的参数个数是否和函数定义一致。这就需要有一些特殊的检测。



Js代码
1.function sum2(a, b) {  
2.    alert(a + b);  
3.}  
4.sum2(1); // NaN  
5.sum2(1, 2); // 3  
6.sum2(1, 3, 5); // 4 
function sum2(a, b) {
    alert(a + b);
}
sum2(1); // NaN
sum2(1, 2); // 3
sum2(1, 3, 5); // 4

看这个例子,仅仅接受两个参数的函数,在调用时可以有任意个参数!但是,它仅取用符合条件的个数,在这里也就是前两个参数。所以,当你传入一个参数时,JavaScript试图将两个数字加起来,结果第二个参数不存在,因此返回值是NaN。第三种情况,实参个数多于形参个数,此时JavaScript只取前两个参数相加。



尽管很不正式,但是可以说,JavaScript的函数参数是不定参数,也就是说,你可以传入任意的参数值。使用JavaScript函数内置的arguments就可以遍历所有传入的参数。比如下面的代码:



Js代码
1.function sum() {  
2.    var total = 0;  
3.    for(var i = 0; i < arguments.length; i++) {  
4.        total += arguments[i];  
5.    }  
6.    alert(total);  
7.}  
8.sum(1, 2);  
9.sum(1, 2, 3); 
function sum() {
    var total = 0;
    for(var i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    alert(total);
}
sum(1, 2);
sum(1, 2, 3);

arguments的行为很像数组,但它并不是数组。可以使用typeof操作符看一下,也可以调用它的constructor属性。



这里有一点需要说明,arguments有个callee属性,可以调用arguments自身所在的函数。也就是说,可以通过这个属性递归调用函数自身。所以,即使是匿名函数,也可以实现递归调用。如:



Js代码
1.function sum(n) {  
2.    if(n <= 1) {  
3.        return 1;  
4.    }  
5.    return n + arguments.callee(n - 1); // 递归调用自身  
6.}  
7.alert(sum(100)); 
function sum(n) {
    if(n <= 1) {
        return 1;
    }
    return n + arguments.callee(n - 1); // 递归调用自身
}
alert(sum(100));

我觉得大家都会知道这个著名问题的答案的。



3. 函数也是对象



回想一下上面的第三个语句,它已经强烈暗示了,函数其实也是对象!那么,作为一个对象,函数应该具有对象的一切特性:添加属性、删除属性、作为返回值等等。是的!JavaScript的函数就是这么样的!



Js代码
1.function hello() {  
2.    alert("Hello!");  
3.}  
4.hello.name = "Tom"; // 添加属性  
5.alert(hello["name"]);  
6.delete hello.name; // 删除属性  
7.alert(hello.name);  
8.// 赋值给变量  
9.var hello1 = function() { alert("hello1"); };  
10.hello1();  
11.// 作为数组元素  
12.function show(x) { alert(x); }  
13.var arr = [show];  
14.arr[0](5);  
15.// 作为函数的参数  
16.function callFunc(func) {  
17.    func();  
18.}  
19.callFunc(function() {  
20.    alert("Inner Function.");  
21.});  
22.// 作为函数的返回值  
23.function show() {  
24.    return function(n) {  
25.        alert("number is " + n);  
26.    };  
27.}  
28.show()(10); 
function hello() {
    alert("Hello!");
}
hello.name = "Tom"; // 添加属性
alert(hello["name"]);
delete hello.name; // 删除属性
alert(hello.name);
// 赋值给变量
var hello1 = function() { alert("hello1"); };
hello1();
// 作为数组元素
function show(x) { alert(x); }
var arr = [show];
arr[0](5);
// 作为函数的参数
function callFunc(func) {
    func();
}
callFunc(function() {
    alert("Inner Function.");
});
// 作为函数的返回值
function show() {
    return function(n) {
        alert("number is " + n);
    };
}
show()(10);

瞧!凡是对象可以做到的,函数统统都能做到!JavaScript中的函数就是对象!



现在我们已经从数组,逐渐开始到对象和函数。这些都是基本概念,后面,我们将对JavaScript的面向对象特性做进一步的介绍。


======================== 5.类 ===============================

类是面向对象程序设计的核心概念之一。一个类代表了具有相似属性的一类事物的抽象。从本篇开始,我们将正式的进入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.} 
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.show = function() {
        alert("Hello, my name is " + this.name + ", my age is " + this.age);
    };
}

这里,我们定义了一个类 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(); 
var bill = new Person("Bill", 30);
alert(bill.name);
alert(bill["age"]);
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}); 
function print(mustHave, person) {
    var defaultPerson = {
        name: "noname",
        age: 0
    };
    if(!mustHave) { // 非空检测
        alert("mustHave should not be null!");
        return;
    }
    person = person || defaultPerson; // 设置默认值
    alert(mustHave + ": name- " + person.name + "; age- " + person.age);
}
print();
print("sth");
print("sth", {name: "new", age: 20});

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


======================== 6.封装 ===============================

封装是面向对象的重要概念之一。如果一个程序没有封装性,也就谈不上什么面向对象。但是,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(); 
function Person(name, age) {
    this.name = name; // 定义一个公有变量
   this.age = age;
    this.show = function() { // 定义一个公有函数
        alert("name: " + name + "; age: " + age);
    }
}
var bill = new Person("Bill", 20);
alert(bill.name);
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, 不存在 
function Person(name, age) {
    var name = name; // 私有属性
   var age = age;
    var show = function() { // 私有函数
        alert("name: " + name + "; age: " + age);
    }
}
var bill = new Person("Bill", 20);
alert(bill.name); // undefined
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 
Person.num = 0; // 静态属性
function Person() {
    this.show = function() {
        alert("num: " + Person.num);
    };
    Person.num++;
}
var bill = new Person();
bill.show(); // 1
var tom = new Person();
tom.show(); // 2
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(); 
function Person(name, age) {
    this.myName = name;
    var myAge = age;
    this.show = function() {
        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却不是那么简单,因为JavaScript是一个脚本语言,我们需要十分关心它的执行效率。下面,我们将会看一下JavaScript面向对象设计的最佳实践。

======================== 7.闭包 ===============================

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



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)); 
function add(a) {
    return function(b) {
        return a + b;
    };
}
var func = add(10);
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()); 
function inc(a) {
    var i = 0;
    return function() {
        return i;
    };
}
var num = inc();
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 
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()); // 1000
alert(p.id); // undefined

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



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



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



from : http://www.iteye.com/forums/41/search?query=javascript%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1
  • 大小: 7.2 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics