JavaScript - 面向对象总结

markdown

面向对象的语言有一个标志,那就是“类”的概念,所谓的“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript语言的对象体系,不是基于“类”的,而是基于 构造函数(constructor)和 原型链(prototype)。

对象的概念

我们从两个层次来理解:
1、“对象”是单个实物的抽象
一本书、一辆汽车、一个人都可以是“对象”,一个数据库、一张网页、一个与远程服务器的连接也可以是“对象”。当实物被抽象成“对象”,实物之间的关系就变成了“对象”之间的

关系,从而就可以模拟现实情况,针对“对象”进行编程。
2、“对象”是一个容器,封装了“属性”(property)和“方法”(method)
所谓“属性”,就是对象的状态;所谓“方法”,就是对象的行为(完成某种任务)。比如,我们可以把动物抽象为 animal 对象,“属性”记录具体是那一种动物,“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

创建对象

虽然 Object 构造函数和对象字面量都可以用来创建单个对象,但这些方式有明显的缺点:使用一个接口创建很多对象,会产生大量的重复代码。未解决这个问题,开发者们不停在探索。

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式。开发人员封装了一个函数,用函数来封装一个对象“模板”,这样可以少些一些代码。

1
2
3
4
5
6
7
8
9
function people(name,age){
return {
'name' : name,
'age' : age
}
}
var people1 = people('Tom',23);
var people2 = people('Peter',24);
alert(people1.age); //23

这种模式虽然解决了创建多个相似对象的问题,这种方法的问题是,people1 和 people2 之间没有内在的联系,不能反映出它们是同一个原型对象的实例。然后,又出现了一种新的模式。

构造函数模式

所谓“构造函数”,其实就是一个普通函数,但是内部使用了 this 变量。对构造函数使用 new 运算符,就能生成实例,并且 this 变量会绑定在实例对象上。

1
2
3
4
5
6
7
function People(name,age){
this.name = name;
this.age = age;
}
var people1 = new People('Tom',23);
var people2 = new People('Peter',24);
alert(people2.name); //Peter

这时 people1 和 people2 会自动拥有一个 constructor 属性,指向他们的构造函数。

1
2
alert(people1.constructor == People); //true
alert(people2.constructor == People); //true

或者也可以使用 instanceof 运算符,验证原型与实例之间的关系:

1
2
alert(people1 instanceof People); //true
alert(people2 instanceof People); //true

构造函数应注意:
1、函数名一般首字母大写,用来和普通函数区分;
2、调用时必须使用 new 运算符,否则 this 将指向全局(属性和方法将属于 window
3、买有 return 语句。

构造函数模式虽然好用,但也不是没有缺点:造成内存浪费。我们给 People 添加一个方法:

1
2
3
4
5
6
7
8
9
10
function People(name,age){
this.name = name;
this.age = age;
this.run = function(){
alert('我会跑!');
}
}
var people1 = new People('Tom',23);
var people2 = new People('Peter',24);
people1.run();

貌似没有什么为题。实际上,run 这个方法应该是每个实例共享的(因为每个人都会跑),但是我们每 new 一次,就会为新的实例重新定义 run 方法,造成内存浪费。

1
alert(people1.run == people2.run); //false

原型模式

Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。

1
2
3
4
5
6
7
8
9
10
11
function People(name,age){
this.name = name;
this.age = age;
}
People.prototype.run = function(){
alert('我会跑!');
}
var people1 = new People('Tom',23);
var people2 = new People('Peter',24);
people1.run(); //我会跑!
alert(people1.run == people2.run); //true

说明 people1 和 people2 的 run() 方法指向相同内存地址,是共享的。减少了内存的使用,提高了运行效率。

原型搜索机制:
当访问一个实例属性时,首先会在实例中搜索该属性,如果找不到,则会继续搜索实例的原型。

继承

一句话,继承 就是让一个对象拥有另一个对象的属性或方法。(我是这么理解的)

原型链

原型链 是 js 继承的主要方法。其主要思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。首先明确一下 构造函数原型实例 的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型的内部指针(对照下图理解这句话)。

markdown

假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是 原型链
原型链的基本模式:(一个构造函数的原型指向另一个构造函数的实例)

1
2
3
4
5
6
7
8
9
10
11
12
function Animal(){}
Animal.prototype.type = function(){
return '动物';
}
function People(name,age){
this.name = name;
this.age = age;
}
People.prototype = new Animal(); //实现继承Animai()
var people1 = new People();
alert(people1.type()); //动物

最终的结果是:people1 指向 People 的原型,People 的原型又指向 Animal 的原型。

需要注意:
注意一: 原型链的顶层为 Object 。这也是所有自定义类型都可以使用 toString()valueOf() 的原因。
注意二: 如果存在继承,那么子对象的实例属于所有所继承的对象。

1
2
3
alert(people1 instanceof People); //true
alert(people1 instanceof Animal); //true
alert(people1 instanceof Object); //true

注意三: 如果需要给子类型添加超类型中不存的方法,或者说要替换掉超类型中的方法,那么给原型添加方法的代码一定要放在替换原型的语句之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Animal(){}
Animal.prototype.type = function(){
return '动物';
}
function People(name,age){
this.name = name;
this.age = age;
}
People.prototype = new Animal(); //实现继承Animai()
People.prototype.type = function(){
return '我也是动物';
}
var people1 = new People();
alert(people1.type()); //我也是动物 //超类型中的方法被覆盖
People.prototype.type = function(){
return '我也是动物';
}
People.prototype = new Animal();
var people1 = new People();
alert(people1.type()); //动物

注意四: 不能使用对象字面量创建原型方法,因为这样会重写原型链。

原型链的问题:
问题一: 前面说过,包含引用类型值得原型属性会被所有实例共享。在通过原型实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成现在的原型属性了。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Color(){
this.color = ['red','blue','green'];
}
function Flower(){}
Flower.prototype = new Color(); //继承Color
var flower1 = new Flower();
flower1.color.push('black');
alert(flower1.color); //red,blue,green,black
var flower2 = new Flower();
alert(flower2.color); //red,blue,green,black

问题二: 没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

借用构造函数

这种技术是为了解决 原型链 中的问题而产生的,他的基本思想是:在子类型构造函数的内部调用超类型的构造函数。方式是使用 call()apply() 方法,将父对象的构造函数绑定在子对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Color(){
this.color = ['red','blue','green'];
}
function Flower(){
Color.call(this); //继承Color
}
var flower1 = new Flower();
flower1.color.push('black');
alert(flower1.color); //red,blue,green,black
var flower2 = new Flower();
alert(flower2.color); //red,blue,green

我们实际上在新创建的 Flower 实例的环境下调用了 Color 构造函数。这样一来,就会在新 Flower 对象上执行 Color 函数中定义的所有对象初始化代码。结果, Flower 的每个实例都会具有自己的 color 属性的副本。
借用构造函数的问题:
方法都在函数中定义,因此函数复用就无从谈起。所以很少单独使用。

组合继承

原型链构造函数 相结合,从而发挥两者之长的继承方式。思路是:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,即通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function People(name){
this.name = name;
this.color = ['red','blue','green'];
}
People.prototype.sayName = function(){
alert(this.name);
}
function Book(name,age){
People.call(this,name); //继承了People的name属性
this.age = age;
}
Book.prototype = new People(); //继承方法
Book.prototype.constructor = Book;
Book.prototype.sayAge = function(){
alert(this.age);
}
var book1 = new Book('js',23);
book1.color.push('black');
alert(book1.color); //red,blue,green,black
book1.sayName(); //js
book1.sayAge(); //23
var book2 = new Book('java',44);
alert(book2.color); //red,blue,green
book2.sayName(); //java
book2.sayAge(); //44

People 构造函数定义了两个属性:namecolor,然后给 People 的原型定义了 sayName() 方法。Book 构造函数在调用 People 构造函数是传入了 name 参数,紧接着又定义了自己的属性 age。然后,将 People 的实例赋值给 Book 的原型,然后又在该新原型上定义了 sayAge() 方法。这样一来,就可以让两个不同的 Book 实例即分别有自己的属性,又可以使用相同方法。