面向对象编程——创建对象的方式

面向对象(Object-oriented,OO)的语言标志:都有类的概念。通过类可以创建任意多个具有相同属性和方法的对象。

一、理解对象

ECMAScript里有一种复杂数据类型,即对象(Object)。ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数”。
对象可以通过执行new操作符后跟要创建的对象类型的名称来创建:
var o = new Object();

二、引用类型

引用类型的值(对象)是引用类型的一个实例。
如:var o = new Object();
这行代码创建了 Object 引用类型的一个新实例,然后把该实例保存在了变量 o 中。使用的构造函数是 Object ,它只为新对象定义了默认的属性和方法。ECMAScript 提供了很多原生引用类型,例如 Object 、Array、Date、RegExp、Function。
到目前为止,我们看到的大多数引用类型值都是 Object 类型的实例;而且, Object 也是ECMAScript 中使用最多的一个类型。虽然 Object 的实例不具备多少功能,但对于在应用程序中存储和传输数据而言,它们确实是非常理想的选择。

三、创建对象的方式

1、对象字面量

var person = {
name: “Carl”,
age: 25,
job: “doctor”,
sayName: function(){
alert(this.name);
};
缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

2、工厂模式

抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节:

function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
alert(this.name);
};
return o;
}

var person1 = creatPerson(“Carl”,20,“Doctor”);
var person2 = creatPerson(“Jim”,22,“Driver”);

可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

3、构造函数模式

可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如,可以使用构造函数模式将前面的例子重写如下。
function Person(name,age,job){
this.name = name;
this.age= age;
this.job= job;
this.sayName = function(){
alert(this.name);
};
}

var person1 = new Person(“Carl”,20,“Doctor”);
var person2 = new Person(“Jim”,22,“Driver”);

按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他 OO 语言,主要是为了区别于 ECMAScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。

person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都
有一个 constructor (构造函数)属性,该属性指向 Person ,如下所示。
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true

我们在这个例子中创建的所有对象既是 Object 的实例,同时也是 Person的实例,这一点通过 instanceof 操作符可以得到验证。
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
之所以同时是 Object 的实例,是因为所有对象均继承自 Object .

缺点:每个方法都要在每个实例上重新创建一遍。若转移到全局创建,如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

4、原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途的包含可以由特定类型的所有实例共享的属性和方法。如下面的例子:
function Person(){
}
Person.prototype.name = “Jim”;
Person.prototype.age= 20;
Person.prototype.job = “Doctor”;
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();

在此,我们将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数变成了空函数。

4.1 理解原型对象
面向对象编程——创建对象的方式
上图展示了Person构造函数、Person的原型属性以及Person现有的两个实例之间的关系。
简单来说:
创建函数——为函数创建prototype属性——该属性指向函数的原型对象;
原型对象的constructor属性——指向prototype所在函数的指针;
构造函数创建新实例——内部 [[Prototype]]指针指向构造函数的原型对象(而非构造函数)。

读取某个对象的属性的过程:
执行一次搜索,目标是具有给定名字的属性。
1、从对象实例本身开始。如果找到了则返回该属性的值;
2、如果没有找到,则继续搜索指针指向的原型对象,如果在原型对象中找到了这个属性,则返回该属性的值。
即:先搜索实例里的属性——没有则搜索原型里的属性。而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。但只会阻止我们访问原型中的那个属性,但不会修改那个属性。
使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。语法:delete person1.name;
即:实例里的属性设置对原型无影响。

使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(从 Object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。语法:person1.hasOwnProperty(“name”);

4.2 原型与in 操作符
“name” in person1;通过对象能够访问给定属性时返回 true ,无论该属性存在于实例中还是原型中。

问题:对于包含基本值的属性,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了:对该属性的修改会在其他实例中反应出来。而实例一般都是要有属于自己的全部属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。

5、组合使用构造函数+原型模式

创建自定义类型的最常见方式。构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性,极大节省内存;此外还支持向构造函数传递参数。是用来定义引用类型的默认模式。如:
function Person(name,age,job){
this.name = name;
this.age= age;
this.job= job;
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
}

var person1 = new Person(“Carl”,20,“Doctor”);
var person2 = new Person(“Jim”,22,“Driver”);

6.动态原型模式

function Person(name, age, job){
//属性
this.name = name;
this.age = age;
this.job = job;
// 方法
if (typeof this.sayName != “function”){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person(“Nicholas”, 29, “Software Engineer”);
friend.sayName();

7、寄生构造函数模式

类似于工厂模式,只是使用了new操作符

8、稳妥构造函数模式

不创建公共属性,方法不引用this对象;不使用new操作符。