面向对象的程序设计

面向对象的语言的标志就是它们有类的概念,通过类可以创建任意多个具有相同属性和方法的对象。JavaScript 中的类的实现是基于其原型继承机制。如果两个实例都从同一个原型对象上继承了属性,就认为是同一个类的实例。

JavaScript 的对象是属性名以及与之对应的值的基本集合。集合是一种数据结构,泳衣表示非重复值的无序集合。

理解对象

JavaScript 是一门基于对象的多泛式语言。可以使用面向过程进行开发:

  • 获取元素,绑定事件、设置样式、完成动画。。。。。。

可以使用面向对象的方式进行开发:

  • 面向(关注于)过程:基于函数,封装函数
  • 面向对象:关注点变成了对象
  • 对象的概念:数据集,功能集: 无序属性的集合,包含基本值,对象或者函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//CEO:安排一个任务给CTO(7天),CTO又把任务给PM(5天),PM又把任务给我了(3天),我去开发这个页面
var ceo = {
assignTaskToCTO: function () { console.log("安排一个任务给CTO"); }
};
var cto = {
assignTaskToPM: function () { console.log("安排一个任务给PM");}
};
var pm = {
assignTaskToMe: function () { console.log("安排一个任务给我"); }
};
var me = {
developWeb:function(){ console.log("我去开发这个页面"); }
};
//开发一个页面
function deleveWeb(){
ceo.assignTaskToCTO();
cto.assignTaskToPM();
pm.assignTaskToMe();
me.developWeb();
}

一个例子: 利用构造函数来定义 “范围类”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 构造函数初始化
function Range(from, to) {
// 存储起始位置和结束位置
this.from = from;
this.to = to;
}
Range.prototype = {
// constructor: Range,
// 如果 x 在范围内,返回 true,否则返回 false
// 这个方法可以比较数字范围,也可以比较字符串和日期范围
includes: function(x) { return this.from <= x && x <= this.to},
// 对于范围内的每一个整数调用函数 f
foreach: function(f) {
for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);
},
// 返回表示这个范围的字符串
toString: function() { return "(" + this.from + "..." + this.to + ")"; }
};
// 举例
var r = new Range(3, 5);
console.log(r.includes(4)); // true
r.foreach(console.log); // 3, 4, 5
console.log(r); // Range{from:3, to:5}

构造函数和类的标识

当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例。初始化对象的状态的构造函数不能作为类的标识,两个构造函数的 prototype 属性可能指向同一个原型对象。那么这连个构造函数的实例是属于同一类型的。

可以使用 r instanceof Range 来判断一个实例是否继承自 Range.prototype。

constructor 属性

每一个 JavaScript 函数都自动拥有一个 prototype 属性,这个属性指向一个对象即称作原型对象,这个对象包含唯一一个不可枚举的属性 constructor。 constructor 的值是一个函数对象。

构造函数的原型中存在预先定义好的 constructor 属性,这意味着通常继承的 constructor 均指代它们的构造函数。

在上面定义的 Range 构造函数的原型由于被另一个对象替换了,所以重写了预定义的 Range.prototype 对象。 Range 的实例的 constructor 没有继承自 Range.prototype 的 constructor 属性。其值变成了 Object。

创建对象的几种方式

使用 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点: 使用一个接口创建很多对象会产生大量的重复代码。

工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建一个包含所有必要信息的 Person 对象
// 可以无数次的调用这个函数,而且每次都会返回一个包含三个属性的方法的对象
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
}
return o;
}
var p1 = createPerson("Hiraku", 23, "JavaScript Engineer");
var p2 = createPerson("Wang", 22, "Java Engineer");
p1.sayName();
p2.sayName();

问题: 并没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
var p1 = new Person("Hiraku", 23, "JavaScript Engineer");
var p2 = new Person("Wang", 22, "Java Engineer");
p1.sayName();
p2.sayName();

和工厂模式的区别:

  • 没有显式的创建对象;
  • 直接将属性和方法赋值给了 this 对象
  • 没有 return 语句
  • 使用 new 关键字创建对象的实例
  • 将构造函数的作用域赋值给新对象(因此 this 就指向了这个对象)
  • 返回新对象
  • p1 和 p2 有继承自 Person.prototype 的属性 constructor,该属性指向 Person

使用构造函数模式的缺点:每个方法都要在每个实例上创建一遍。如 p1 和 p2 都有一个名为 sayName() 方法,但是两个方法不是同一个 Function 的实例。创建两个完成相同任务的 Function 实例没有必要,这样会浪费内存。

原型模式

原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

1
2
3
4
5
6
7
8
9
function Person() {}
Person.prototype.name = "Hiraku";
Person.prototype.age = 22;
Person.prototype.job = "JavaScript Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
}
var p1 = new Person();
p1.sayName();

好处:可以让所有对象实例共享它所包含的属性和方法。

当给对象的实例添加一个属性时,这个属性会屏蔽原型对象中保存的同名属性。

更简单的原型语法

1
2
3
4
5
6
7
8
9
function Person(){}
Person.prototype = {
name: "Hiraku",
age: 23,
job: "JavaScript Engineer",
sayName: function() {
console.log(this.name);
}
};

这时 constructor 属性不再指向 Person 了,constructor 变成了新对象的 constructor,是 Object 构造函数。

这时,需要还原构造器

1
2
3
4
5
6
7
8
9
10
function Person(){}
Person.prototype = {
constructor: Person,
name: "Hiraku",
age: 23,
job: "JavaScript Engineer",
sayName: function() {
console.log(this.name);
}
};

注: 以上方式重置 constructor 属性会导致它的 [[enumerable]] 特性被设置为 true。 默认情况下,原生的 constructor 属性是不可枚举的,因此,可以使用 ECMAScript5 中的 Object.defineProperty() 方法来设置。

1
2
3
4
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});

原型的动态特性

1
2
3
4
5
6
7
8
9
function Person() {}
var friend = new Person();
Person.prototype = {
name: "Hiraku",
sayName: function() {
console.log(this.name);
}
}
friend.sayName(); // 报错

原因是重写了原型对象,把原型对象修改为另一个对象就等于切断了构造函数与最初原型之间的联系。 实例中的指针仅仅指向原型,而不指向构造函数。

原生的对象原型

原型模式不仅仅体现在创建自定义类型放没放,就连所有原生的引用类型,都采用这种模式创建的。所有原生原生引用类型都在其构造函数的原型上定义了方法。

通过原生原型对象,不仅可以取得默认方法的引用,而且还可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。

但是,不建议在编程时修改原生对象的原型。

原型模式的缺点

省略了构造函数初始化,所有实例默认情况下都将取得相同的属性值。还有,原型模式的最大问题是由其共享的本性所导致。

组合使用构造函数模式和原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Herschal", "Camile"];
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
}
}
var p1 = new Person("Hiraku", 23, "JavaScript Engineer");
var p2 = new Person("Wang", 22, "Java Engineer");
p1.friends.push("Van");
console.log(p1.friends); // "Herschal, Camile, Van"
console.log(p2.friends); // "Herschal, Camile"
console.log(p1.friends === p2.friends); // false
console.log(p1.sayName === p2.sayName); // true
p1.sayName();
p2.sayName();

这种方式是使用最广泛的。

动态原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Herschal", "Camile"];
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
console.log(this.name);
};
}
}
var p1 = new Person("Hiraku", 23, "JavaScript Engineer");
p1.sayName();

这里在对原型做的修改,能立即在所有实例中得到反映。

寄生构造函数模式

基本思路是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
var p1 = new Person("Hiraku", 23, "JavaScript Engineer");
p1.sayName();

关于寄生构造模式,首先,返回对象与构造函数或者构造函数的原型属性之间没有关系。也就是说,构造函数返回到对象与在构造函数外部创建的对象没有什么不同。不能依赖 instanceof 操作符来确定对象的类型。

稳妥构造函数模式

首先介绍稳妥对象,稳妥对象是指没有公共属性,而且其它方法也不引用 this 的对象。稳妥模式适合在一些安全的环境中,或者在防止数据被其它应用程序改动时使用。稳妥模式遵循寄生模式,但有两点不同。

  • 新创建对象的实例方法不引用 this;
  • 不使用 new 操作符来调用构造函数。
1
2
3
4
5
6
7
8
9
function Person(name, age, job) {
var o = new Object();
o.sayName = function() {
console.log(name);
};
return o;
}
var p1 = Person("Hiraku", 23, "JavaScript Engineer");
p1.sayName();
感谢您的支持!