JavaScript高级程序设计笔记(7)-面向对象的程序设计

ECMA-262定义对象:

无序属性的集合,其属性可以包含基本值、对象或者函数。

每个对象都是基于一个引用类型创建的。

创建对象

工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
return o;
}
var person1 = createPerson( 'Tom', 18, 'Engineer');
var person2 = createPerson('Lily', 18,
'Teacher');

缺点: 没有解决对象识别问题,即怎样知道一个对象的类型。

构造函数模式

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

var person1 = new Person('Tom', 18, 'Engineer');
var person2 = new Person('Lily', 18, 'Teacher');

经历了一下四个步骤:
1、 创建一个新对象;
2、 将构造函数的作用于赋给新对象(this指向了这个新对象);
3、执行构造函数中的代码(为新对象添加属性);
4、返回新对象。

上例中,person1和person2保存着Person的一个不同的实例,但它们拥有一个constructor属性,该属性指向person。

1
2
3
4
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true

以这种方式定义的构造函数是定义在Global对象上中的(在浏览器中是window对象)。

1、 将构造函数当做函数

构造函数与普通函数唯一的区别在于调用方式不同。任何函数,只要通过new操作符来调用,那它就可以作为构造函数,如果不通过new操作符,那它跟普通函数没有区别。

2、 构造函数问题
每个方法都要在每个实例上重新创建一遍,person1和person2都有一个名为sayName()的方法,但两个方法不是同一个Function的实例。ECMAScript中函数式对象,因此每定义一个函数,也就是实例化一个对象。因此,构造函数可以这样定义:

1
2
3
4
5
6
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function('alert(this.name');//与声明函数在逻辑上是等价的。
}

显然,每个Person实例都包含一个不同的Function实例来显示name。这种方式创建函数,会导致不同的作用域链和标识符解析,但新创建Function新实例的机制仍然相同,因此,不同实例上的同名函数式不等的。

1
console.log(person1.sayname == person2.sayName);//false

原型模式

每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以有特定类型的所有实例共享的属性和方法。prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处就是,可以让所有实例共享它所包含的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(){}
Person.prototype.name = 'test';
Person.prototype.age = 18;
Person.prototype.job = 'fe';
Person.prototype.sayName = function(){
console.log(this.name);
}

var person1 = new Person();
console.log(person1.sayName());//'test'
var person2 = new Person();
console.log(person2.sayName());//'test'

var person3 = new Person();
person3.name = 'Tom';
console.log(person3.sayName());//'Tom',来自实例
console.log(person2.sayName());//'test', 来自原型

原型对象测试

1
2
3
Person.prototype.isPorotypeOf(person1);//true

Object.getPrototypeOf(person1 == person.prototype);//true

在对象实例中添加属性,这个属性就是屏蔽原型对象中的同名属性,但不会修改该那个属性。即使在实例中将属性设置为null,也只会在实例中设置该属性。使用delete操作符可以完全删除实例属性,从而能够重新访问原型中的属性。

hasOwnProperty()方法可以检测一个属性是存在于实例中是还是原型中。只有属性存在于对象实例中,才会返回true。

原型与 in 操作符

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person(){}
Person.prototype.name = 'test';
Person.prototype.age = 18;
Person.prototype.job = 'fe';
Person.prototype.sayName = function(){
console.log(this.name);
}

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

console.log(person1.hasOwnProperty('name'));//false
console.log('name' in person1); //true

person1.name = 'dd';
console.log(person1.name);//'dd'-来自实例
console.log(person1.hasOwnProperty('name'));//true
console.log('name' in person1); //true

console.log(person2.name);// 'test'-来自原型
console.log(person2.hasOwnProperty('name'));//false
console.log('name' in perosn2);// true

Object.keys()枚举对象上所有可枚举的实例属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(){}
Person.prototype.name = 'test';
Person.prototype.age = 18;
Person.prototype.job = 'fe';
Person.prototype.sayName = function(){
console.log(this.name);
}
var keys = Object.keys(Person.prototype);
console.log(keys);//['name', 'age', 'job', 'sayName']

//通过实例调用
var p1 = new Person();
p1.name = 'Lilei';
p1.age = 18;
var p1keys = Object.keys(p1);
console.log(p1keys);//['name', 'age']

Object.getOwnPropertyNames()可以得到所有实例属性,无论它是否可枚举。

1
Object.getOwnPropertyNames(Person.prototype);//["constructor", "name", "age", "job", "sayName"]

原型的动态性

1
2
3
4
5
var friend = new Person();
Person.prototype.sayHi = function(){
alert('hi');
}
friend.sayHi();//'hi'

这是因为实例与原型之间的松散连接关系。可以随时为原型添加属性和方法,并且修改后能够立即在所有对象实例中反映出了。
但是如果重写整个原型对象,情况就不一样了。调用构造函数时回味实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另外一个对象,就等于切断了构造函数与最初原型之间的联系。实例中的指针仅指向原型,而不指向构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
function Person(){};
var friend = new Person();
Person.prototype = {
constructor: Person,
name: 'test',
age: 18,
job: 'fe',
sayName: function(){
console.log(this.name);
}
}
friend.sayName();//error

原型对象的问题

最大问题是由其共享的本性导致的。

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

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Tom','Lilei'];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name);
}
}

var person1 = new Person('test', 18, 'fe');
var person2 = new Person('p2', 18, 'fe');

person1.friends.push('Lily');
console.log(person1.friends === preson2.friends);//false
console.log(preson1.sayName === person2.sayName);//true

动态原型模式

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;
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
console.log(this.name);
}
}
}
var friend = new Person('Test', 18, 'fe');
friend.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 friend = new Person('Tom', 18, 'fe');
friend.sayName();//'Tom'

稳妥构造函数模式

继承

ECMAScript支持实现继承,主要依靠原型链来实现。

原型链

将原型链作为实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数、原型和实例的关系:每个构造函数都要一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 instance = new SubType();
alert(instance.getSuperValue());//true

原型链问题:

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

function SubType(){}

SubType.prototype = new SuperType();

var instance1 = new SuperType();
instance1.colors.push('black');
alert(instance1.colors);//'red,green,blue,black'

var instance2 = new SuperType();
alert(instance2.colors);//'red,green,blue,black'

借用构造函数

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

function subType(){
//继承了SuperType
SuperType.call(this);
}
var instance1 = new SuperType();
instance1.colors.push('black');
alert(instance1.colors);//'red,green,blue,black'

var instance2 = new SuperType();
alert(instance2.colors);//'red,green,blue'

使用apply或call方法,通过call()或apply()方法,实际上在新创建的SubType实例环境下调用了SuperType构造函数。

组合继承

将原型链和借用构造函数的技术组合到一起,发挥二者之长的继承模式。思路:使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。通过原型上定义方法实现了函数复用,又能保证没给个实例都有自己的属性。

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 SuperType(name){
this.name = name;
this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}

function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}

var instance1 = new SubType('test', 18);
instance1.colors.push('black');
alert(instance1.colors);//'red,blue,green,black'
instance1.sayName();//'test'
instance1.sayAge();//18

var instance2 = new SubType('fe', 17);
alert(instance2.colors);//'red,blue,green'
instance2.sayName();//'fe'
instance2.sayAge();//17

原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function object(o){
function F(){};
F.prototype = o;
return new F();
}

var person = {
name: 'fe',
friends: ['js','html', 'css']
}

var anotherPerson = object(person);
anotherPerson.name = 'hello';
anotherPerson.friends.push('java');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'world';
yetAnotherPerson.friends.push('node');

alert(person.friends);

寄生式继承

寄生组合式继承