面向对象的程序设计

面向对象的程序设计

理解对象

常见创建对象方式

1. 创建一个 Object 实例

1
2
3
4
5
6
7
var person = new Object();
person.name = 'andy';
person.age = 12;

person.sayName = function() {
console.log(this.name);
}

2. 对象字面量

1
2
3
4
5
6
7
8
var person = {
name: 'andy',
age: 12,

sayName: function() {
console.log(this.name);
}
}

属性类型

ECMA-262 第5版在定义只有内部采用的特性时,描述了属性的各种特征, ECMA-262定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们.

ECMAScript中有两种属性: 数据属性访问器属性

数据属性

  • [[Configurable]]: 表示能否通过delete删除属性从而重新定义属性, 默认true
  • [[Enumerable]]: 表示能否通过 for-in 循环返回属性, 默认true
  • [[Writable]]: 表示能否修改属性的值, 默认true
  • [[Value]]: 包含这个属性的数据值. 读取属性值的时候, 从这个位置读,写入时, 把新值保存在这个位置,默认undefined

要修改属性默认的特性, 必须使用ECMAScript 5的Object.defineProperty()方法.

1
2
3
4
5
6
7
8
9
var person = {};
Object.defineProperty(person, 'name', {
writable: false,
value: 'andy'
});

console.log(person.name): // andy
person.name = 'qiqi';
console.log(person.name): // andy

该属性的值配置为不可修改, 如果尝试为它指定新值,在非严格模式下,赋值操作会被忽略,在严格模式下,赋值操作将会导致抛出错误.

1
2
3
4
5
6
7
8
9
10
11
var person = {};
Object.defineProperty(person, 'name', {
configurable: false,
value: 'andy'
})

// 抛出错误
OBject.defineProperty(person, 'name', {
configurable: true,
value: 'andy'
})

也就是说, 可以多次调用Object.defineProperty()方法修改同一个属性,但在把configurable特性设置为false之后就会有限制了.

*IE8是第一个实现Object.defineProperty()方法 的浏览器版本.然而这个版本存在诸多限制:所以不建议使用.*

访问器属性

访问器属性不包含数据值, 它们包含一对儿gettersetter函数,在读取访问器属性时,会调用getter函数, 这个函数负责返回有效的值,在写入访问器属性时,会调用setter函数并传入新值.

  • [[Configurable]]: 表示能否通过delete删除属性从而重新定义属性, 默认true
  • [[Enumerable]]: 表示能否通过 for-in 循环返回属性, 默认true
  • [[Get]]: 在读取属性时调用的函数,默认值undefined
  • [[Set]]: 在写入属性时调用的函数, 默认值为undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var book = {
_year: 2004,
edition: 1
}

Object.defineProperty(book, 'year', {
get: function() {
return this._year;
},
set: function(newValue) {
this._year = newValue;
this.edition += newValue - 2004;
}
})

book.year = 2005;
console.log(book.edition): // 2

以上代码指定了book对象中year属性的访问器属性, 其实也不一定要同时制定gettersetter, 只指定getter意味着属性是不能写,只指定setter意味着属性不能读.

Object.defineProperties(): 利用这个方法可以通过描述符一次定义多个属性.

Object.getOwnPropertyDescriptor(): 取得给定属性功能的描述符.

创建对象

1. 工厂模式

1
2
3
4
5
6
7
8
9
10
11
function createPerson(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
return this.name;
}
return o;
}

var person = createPerson('andy', 12);

如上所示,工厂模式通过传入参数来赋值给对象相应的属性, 从而返回相似的对象, 这种模式虽然解决了多个相似对象的问题, 但是没有解决对象识别的问题(即怎样知道一个对象的类型);

2. 构造函数模式

ECMAScript中的构造函数可用来创建特定类型的对象.像Object和Array这样的原生构造函数, 在运行时会自动出现在执行环境中,此外,也可以创建自定义的构造函数

1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
return this.name;
}
}

var person1 = new Person('andy', 12);
var person2 = new Person('qiqi', 12);

要创建Person的新实例,必须使用new操作符,以这种方式调用构造函数实际上会经历以下四个步骤

  1. 创建一个新对象
  2. 将构造函数的作用域给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型, 而这正是构造函数模式胜过工厂模式的地方.

1
person1.sayName === person2.sayName; // false

构造函数的问题: 使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍. 但是创建两个完成同样任务的Function实例的确没有必要,况且有this对象在,根本不用在执行代码前就把函数绑定到特定的对象上面.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name, age) {
this.name = name;
this.age = age;

this.sayName = sayName;
}

function sayName() {
console.log(this.name);
}

var person1 = new Person('andy', 12);
var person2 = new Person('qiqi', 11);

console.log(person1.sayName === person2.sayName); // true

在这个例子中,我们把sayName()函数的定义转移到了构造函数外部.而在构造函数内部,我们将sayName属性设置成全局的sayName函数, 这样以来,就解决了方法的实例重复创建的问题, 但是当这样的全局函数变多时, 将会对全局变量造成污染.

3. 原型模式

我们创建的每个函数都有一个 prototype属性,这个属性时一个指针, 指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法. 我们可以将这些信息添加到原型对象中,从而实现共享.

1
2
3
4
5
6
7
8
9
10
function Person() {};

Person.prototype.name = 'andy';
Person.prototype.age = 12;
Person.prototype.sayName = function() {
console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();

这样实现的问题在于它们的原型对象时共享的, 即person1person2访问的都是同一组属性和同一个sayName()函数.

判断属性是来自于原型还是实例
1
2
3
4
5
//  该方法可用来判断name属性是来自于原型上的还是实例上的.
person1.hasOwnProperty('name'); // false

// 只要能通过原型链找到就返回true
console.log(name in person1); // true

Example: 判断来自于原型

1
2
3
function hasPrototypePtoperty(object, name) {
return !Object.hasOwnProperty(name) && (name in object);
}

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

1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.sayName = function() {
console.log(this.name);
}

var person1 = new Person('anqi', 12);

这种构造函数与原型混成的模式, 是目前在ECMAScript中使用最广泛, 认同度最高的一种创建自定义类型的方法. 可以说, 这是用来定义饮用类型的一种默认模式.

5. 动态原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age) {
this.name = name;
this.age = age;

if (typeof this.sayName != 'function') {
Person.prototype.sayName = function() {
console.log(this.name);
}
}
}

var person1 = new Person('andy', 12);
person1.sayName();

这里只在sayName()方法不存在的情况下, 才会将它添加到原型中.这段代码只会在初次调用构造函数时才会执行.此后, 原型已经出实话,不需要再做什么修改了.

6. 寄生构造函数模式

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

var person1 = new Person('andy', 12);
person1.sayName();

这种模式的思想是创建一个函数, 该函数的作用仅仅是封装对象的代码,然后再返回新创建的对象; 在这里例子中,除了使用new操作符并把使用的包装函数叫做构造函数之外, 这个模式跟工厂模式其实是一摸一样的.构造函数在不返回值的情况下,默认会返回新对象实例.而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值.

这个模式可以在特殊的情况下用来为对象创建构造函数,假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array

构造函数,因此可以使用这个模式.

1
2
3
4
5
6
7
8
9
10
11
function SpecialArray() {
var values = new Array();
values.push.apply(values, arguments);
values.toPipedString = function() {
return this.join('|');
}
return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
console.log(colors.toPipedString()); // red|blud|green

7. 稳妥构造函数模式

1
2
3
4
5
6
7
function Person(name, age) {
var o = new Object();
o.sayName = function() {
console.log(name);
}
return o;
}

注意,以这种模式创建的对象中, 除了使用sayName()方法之外,没有其他办法访问name的值.这种模式适合用于一些比较安全的环境中, 不能使用thisnew.

继承

1. 原型链实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType() {
this.property = true;
}

SuperType.prototype.getSuperValue = function() {
return this.property;
}

function SubType() {
this.subproperty = false;
};

SubType.prototype = new SuperType();


SubType.prototype.getSubValue = function() {
return this.subproperty;
}

var aa = new SubType();
console.log(aa.getSuperValue()); // true

原型链虽然强大,可以用它来实现继承,但它也存在一些问题.其中,最主要的问题来自包含引用类型值的原型, 引用类型值的原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因.在通过原型来实现继承时,原型实际上会变成另一个类型的实例,于是,原先的实例属性也就顺理成章的变成了现在的原型属性了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}

function SubType() {}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // red,blue,green,black

var instance2 = new SubType();
console.log(instance2.colors); // red,blud,green,black

原型链的第二个问题时: 在创建子类型的实例时,不能向超类型的构造函数中传递参数.实际上,应该说时没有办法在不影响所有对象实例的情况下, 给超类型的构造函数传递参数.

2. 构造函数继承

这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}

function SubType() {
SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // red,blue,green,black

var instance2 = new SubType();
console.log(instance2.colors); // red,blud,green

借用构造函数模式, 还可以实现子类型构造函数中向超类型构造函数传递参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType(name) {
this.name = name;
}

function SubType() {
SuperType.call(this, 'andy');

this.age = 12;
}

var instance = new SubType();
console.log(instance.name); // andy
console.log(instance age); // 12

如果仅仅是借用构造函数,那么无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用也就无从谈起了.而且超类型原型中国呢定义的方法, 对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式.

3. 组合继承

组合继承,有时候也叫做伪经典继承,指的是将原型链和构造函数继承的结束和到一起,从而发挥二者之长的一种继承模式. 其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承, 这样, 既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性.

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
29
30
31
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blud', 'green'];
}

SuperType.prototype.sayName = function() {
console.log(this.name);
}

function SubType(name, age) {
SuperType.call(this, name);

this.age = age;
}

SubType.prototype = new SuperType();

SubType.prototype.sayAge = function() {
console.log(this.age);
}

var instance1 = new SubType('andy', 12);
instance1.colors.push('black');
console.log(instance1.colors); // red,blue,greem,black
instance1.sayName(); // andy
instance1.sayAge(); // 12

var instance2 = new SubType('qiqi', 11);
console.log(instance2.colors); // red,blue,green
instance2.sayName(); // qiqi
instance2.sayAge(); // 11

继承组合避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式.而且,instanceof和isP rototypeOf()也能够用于识别基于组合继承创建的对象.

4. 原型式继承

该模式的思想是: 原型可以基于已有的对象创建新对象,同时还不比因此创建自定义类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
function object(o) {
function F(){};
F.prototype = o;
return new F();
}

var person = {
name: 'andy',
age: 12
}

var aa = object(person);
var bb = object(person);

personaa对象的基础,也就是说personaa对象的原型,通过这种方式实现的继承,它们的原型是共享的, 也就是说aabb的原型是相同的,这与原型链实现继承效果是相似的.

ECMAScript 5通过新增Object.create()方法规范了原型式继承,这个方法接收两个参数,一个用作新对象原型, 一个为新对象定义额外属性的对象,在传入一个参数的情况下, Object.create()与object()方法的行为相同.

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承式完全可以胜任的, 不过,包含引用类型值的属性是中都会共享相应的值,就像使用原型模式一样.

5. 寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以·某种方式来增强对象,最后再像真的式它做了所有工作一样返回对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createAnother(original) {
var clone = object(original);

clone.sayHi = function() {
console.log('hi');
}

return clone;
}

var person = {
name: 'andy',
age: 12
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式.

6. 寄生组合式继承

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
function inheritPrototype(subtype, superType) {
var prototype = object(superType.prototype);
prototype.constructor = superType;
subType.prototype = prototype;
}

function SuperType(name) {
this.name = name;
}

SuperType.prototype.sayName = function() {
console.log(this.name);
}

function SubType(name, age) {
SuperType.call(this, name);

this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {
console.log(this.age);
}

这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上创建不必要的,多余的属性,与此同时,原型链还能保持不变,还能够正常使用instanceofisPrototypeOf()

这是引用类型最理想的继承方式.