下面是我的关于JavaScript中面向对象编程的学习笔记,希望对大家学习这个部分有帮助,主要参考了一本书:
内容主要分成两个部分,第一部分是对象的产生,第二部分是继承。
理解对象
JavaScript是一种面向对象的语言,但是与别的语言不同,它没有类的概念,如果你有在其他面向对象的语言下的编程经验,这一点会给你带来不小的困扰。
对象是什么?
我们可以把对象想象成散列表,或者更形象一点对象就是一组数据和方法的集合。数据在对象中叫做属性,它有属性名和属性值,属性的值可以是其他对象或者函数。当属性的值是函数的时候,这个”数据”就是方法。
我们看一个例子,一个叫做person的对象有3个属性(name,age,job)和1个方法(sayName)。1
2
3
4
5
6
7
8var person = new Object();
person.name = "Jodan";
person.age = 52;
person.jod = "basketball player";
person.sayName = function(){
alert(this.name);
}
创建对象的方法
原始方法
上面这个例子使用的就是最原始的创建对象的方法,这种方法十分的繁琐。于是就诞生了一个不那么原始的使用对象字面量的原始方法:1
2
3
4
5
6
7
8
9var person = {
name : "Jodan",
age : 52,
jod : "basketball player",
sayName : function(){
alert(this.name);
}
}; //对象字面量方法是一条语句,所以不要忘记语句的结尾分号
如果我需要再创建一个叫Jobs的person对象怎么办?如果我需要创建一个班级几十个人的person对象怎么办?
利用原始方法创建很多对象,就会产生大量重复的代码。于是我们就想一种方案,能不能把创建对象的方法封装起来,封装成一个函数,每次创建对象只要调用函数就可以了。人们创造了一种叫做工厂模式的方法。
工厂模式
这种模式抽象了我们创建对象的具体过程。这种模式的思路是编写一个函数,在函数中创建一个对象,为这个对象添加属性和方法,最后返回这个对象。
1 | function createPerson(name, age, job){ |
现在我们可以很方便的产生很多person对象了,但是我现在想知道这个和别人名字不太一样的Jordan对象是不是person对象的一个实例。倒是有一个instanceof操作符,但是它判断的是某个实例是否处于某个构造函数的原型链上。这里哪有什么原型链,什么构造函数,就只有一个工厂。
构造函数模式
在需要识别对象的情况下,我们需要使用构造函数模式。什么是构造函数呢?如果一个函数使用new操作符进行调用,它就是构造函数,否则就是一个普通的函数。new操作符返回在构造函数中创建的对象。1
2
3
4
5
6
7
8
9
10
11
12
13function Person(name, age, job){
//如果想把一个函数作为构造函数使用,记得将首字母大写,将其与普通函数区分。如果构造函数没有使用new操作符可能会带来灾难性的结果。
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
//new操作符非常重要,它会将Person函数中的this指向函数的作用域,如果没有new,函数里的this指向的是window作用域。
调用构造函数会经历4个步骤:
- 创建一个新对象
- 将this绑定到这个对象上
- 使用this为这个对象添加属性和方法
- 返回这个对象
利用构造函数创建的对象,可以使用instanceof操作符判断这个对象的”来历”。1
2
3
4alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
构造模式真好用啊!哦,好吧,不得不说它也有一个毛病。上面的person1和person2对象都有一个sayName方法,这个方法对他们两个来说是一样的,但是在使用构造函数构造他们的时候我们给他们分别创造了一个副本,当方法数量增加对象数量增加的时候,对于内存空间的浪费巨大。
一种解决方案是,将方法移出构造函数,放在全局空间,对象实例维持一个指向这个方法的指针。但是这种解决方案又会带来新的问题,将构造函数的方法都移到全局空间,那么很快全局空间很快就混乱不堪了,而且在全局空间中定义的函数,只被那些实例调用,让全局作用域有点名不副实。
还有一种解决方案是利用prototype(原型)属性,这个属性是一个指针,指向由一些实例共享的属性和方法。在这个例子中,使用prototype属性可以使person1和person2共享属性和方法,这个prototype怎么用呢?
原型模式
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个包含可以由这个函数构造出来的对象共享的属性和方法的对象。从字面上理解,就是通过调用构造函数创建的对象有一个原型指针指向原型对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
上图展示了Person构造函数,Person的原型属性以及Person的两个实例之间的关系。Person构造函数有一个prototype属性,这个属性的值是一个prototype对象,这个对象又有constructor, name, age, job属性和 sayName方法。person1和person2有一个prototype指针指向Person构造函数的prototype对象。
利用原型模式我们就可以让person1和person2共享一些属性和方法。在这里person1和person2共享了所有Person构造函数赋予他们的属性和方法,在实际使用中我们并不希望如此。我们希望他们有各自的属性,和方法同时又共享一些属性和方法,怎么做呢?就是将原型模式和构造函数模式组合起来!
组合模式
看一个例子我们就知道该如何使用这种最常用的模式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
很简单吧,就是将不想要共享的属性和方法放到构造函数中,而希望被共享的就放在构造函数的原型对象中。
继承的知识放在下一篇文章中介绍。