JS 面向对象的程序设计

面向对象的语言有一个标志,那就是都有类的概念。通过类可以创建任意多个具有相同属性和方法的对象。ECMAScript中没有类的概念,因此它的对象与纯面向对象语言中的对象是不同的。

ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数”。可以简单理解为JS的对象是一组无序的值,其中的属性或方法都有一个名字,根据这个名字可以访问相映射的值(值可以是基本值/对象/方法)。

一、理解对象

基于Object对象

创建自定义对象最简单的方法就是创建一个Object的实例,然后再为它添加属性和方法:

1
2
3
4
5
6
7
8
var person = new Object();
person.name = 'Nicholas';
person.age = 29;
person.job = 'Software Engineer';

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

字面量方式

1
2
3
4
5
6
7
8
var person = {
name : 'Nicholas',
age : 29,
job : 'Software Engineer',
sayName : function(){
console.log(this.name);
}
};

JS的对象可以使用.操作符动态的扩展其属性,可以使用delete操作符或将属性值设置为undefined来删除属性。如下:

1
2
3
4
person.newAtt='new Attr'; //添加属性
console.log(person.newAtt); //new Attr
delete person.age;
console.log(person.age); //undefined(删除属性后值为undefined);

二、创建对象

虽然Object构造函数或对象字面量都可以创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量重复代码。为解决这个问题,人们开始使用工厂模式的一种变体:

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。因为ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定的接口创建对象的细节,如下:

1
2
3
4
5
6
7
8
9
10
11
12
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
return this.name;
}
return o; //使用return返回生成的对象
}
var person1 = createPerson('Nicholas', 29, 'SoftWare Engineer');
var person2 = createPerson('Greg', 27, 'Doctor');

创建对象交给一个工厂方法来实现,可以传递参数,但主要缺点是无法识别对象类型,因为创建对象都是使用Object的原生构造函数来完成的。

构造函数模式

ECMAScript中的构造函数可用来创建特定类型的对象。像ObjectArray这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法,如下:

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() {
console.log(this.name);
}
}

var person1 = new Person('Nicholas', 29, 'SoftWare Engineer');
var person2 = new Person('Greg', 27, 'Doctor');

使用自定义的构造函数(与普通函数一样,只是用它来创建对象),定义对象类型(如:Person)的属性和方法。它与工厂方法区别在于:

  • 没有显式地创建对象;
  • 直接将属性和方法赋值给了this对象;
  • 没有return语句。

此外,要创建Person的实例,必须使用new关键字,以Person函数为构造函数,传递参数完成对象创建;实际创建经过以下4个过程:

  1. 创建一个对象;
  2. 将函数的作用域赋给新对象(因此this指向这个新对象,如:person1);
  3. 执行构造函数的代码;
  4. 返回该对象。

在前面例子的最后,person1person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向创建这个实例的函数对象,即Person,如下:

1
2
console.log(person1.constructor === Person); //true
console.log(person2.constructor === Person); //true

对象的constructor属性最初是用来标书对象类型的。针对对象类型的检测,还是instanceof操作符更可靠:

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

console.log(person2 instanceof Person); //true;
console.log(person2 instanceof Object); //true;

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

1.将构造函数当做函数

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。构造函数本身也是函数,不存在特殊定义特殊语法。任何函数,只要通过new操作符调用,那它就可以作为构造函数,反之,不通过new,那就跟普通函数没什么两样。如下:

1
2
3
4
5
6
7
8
9
10
11
12
//当作构造函数使用
var person = new Person('Nicholas', 29, 'SoftWare Engineer');
person.sayName(); //"Nicholas"

//当作普通函数调用,并且作用域在window上
Person('Greg',27,'Doctor');
window.sayName(); //"Greg"

//在另一个对象的作用域中调用
var o = {};
Person.call(o, 'chan', 25, '切图仔');
o.sayName(); //"chan"
2.构造函数的问题

构造函数模式虽然好用,但并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。前面例子中,person1person2都有一个名为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('console.log(this.name)'); //与声明函数在逻辑上是等价的
}

上述代码,创建多个实例时,会重复调用new Function();创建多个函数实例,这些函数实例还不是一个作用域中,当然这一般不会有错,但这会造成内存浪费。当然,可以在函数中定义一个sayName = sayName的引用,而sayName函数在Person外定义,这样可以解决重复创建函数实例问题,但在效果上并没有起到封装的效果,如下所示:

1
2
3
4
5
6
7
8
9
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName; //指向外部全局函数的指针
}
function sayName() {
console.log(this.name);
}

原型模式

JS中每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按照字面意思理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象,即:它是所有通过new操作符使用函数创建的实例的原型对象。原型对象最大特点是,所有对象实例共享它所包含的属性和方法,换句话说,不必在构造函数中定义对象实例的信息,可以将这些信息直接添加到原型对象中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
function Person(){
// 空的构造函数
}
Person.prototype.name = 'Nicholas';//使用原型来添加属性
Person.prototype.age = 29;
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
console.log(person1.sayName()); //"Nicholas"
var person2 = new Person();
console.log(person1.sayName === person2.sayName); //true;共享一个原型对象的方法
1.理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。拿前面的例子来说,Person函数有一个prototype属性,Person.prototype有一个constructor属性,这个属性指向指向Person。如下:

1
2
3
Person.prototype //Person {name: "Nicholas", age: 29}
Person.prototype.constructor //Person()
Person.prototype.constructor == Person //true

创建自定义构造函数后,其原型对象默认只会取得constructor属性,其他属性都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但FFSafariChrome都暴露了一个__proto__;不过,要明确真正重要的一点是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

实例属性或方法的访问过程是一次搜索过程:

  • 首先从对象实例本身开始,如果找到属性就直接返回该属性值;
  • 如果实例本身不存在要查找的属性,就继续搜索指针指向的原型对象,在其中查找给定名字的属性,如果有就返回。

基于以上分析,原型模式创建的对象实例,其属性是共享原型对象的;但也可以自己实例中再进行定义,在查找时,就不从原型对象获取,而是根据搜索原则,得到本实例的返回;简单来说,就是实例中属性会屏蔽原型对象中的属性;

2.原型与in操作符

一句话:无论原型中属性,还是对象实例的属性,都可以使用in操作符访问到;要想判断是否是实例本身的属性可以使用object.hasOwnProperty('attr')来判断;

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
function Person(){
//空的
}
Person.prototype.name = 'chan';
Person.prototype.age = 29;
Person.prototype.job = '切图仔';
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 = 'long';
console.log(person1.name); //"long" ————来自实例
console.log(person1.hasOwnProperty('name')); //true
console.log('name' in person1); //true

console.log(person2.name); //"chan" ————来自原型
console.log(person2.hasOwnProperty('name')); //false
console.log('name' in person2); //true

delete person1.name;
console.log(person1.name); //"chan" ————实例属性已被删除,来自原型
console.log(person1.hasOwnProperty('name')); //false
console.log('name' in person1); //true
3.原生对象中原型

原型模式的重要性不仅体现在创建自定义类型,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(ObjectArrayString、等等)都是其构造函数的原型上定义了方法。

原生对象中原型与普通对象的原型一样,可以添加/修改属性或方法,如以下代码为所有字符串对象添加去左右空白原型方法:

1
2
3
4
5
String.prototype.trim = function(){
return this.replace(/^\s+/,'').replace(/\s+$/,'');
};
var str = ' word space ';
console.log('!'+str.trim()+'!'); //!word space!
4.原型对象的问题

原型模式的缺点,它省略了为构造函数传递初始化参数,这在一定程序带来不便;另外,最主要是当对象的属性是引用类型时,它的值是不变的,总是引用同一个外部对象,所有实例对该对象的操作都会其它实例:

1
2
3
4
5
6
7
8
9
function Person() {
//空函数
}
Person.prototype.name = 'Jack';
Person.prototype.lessons = ['Math','Physics'];
var person1 = new Person();
person1.lessons.push('Biology');
var person2 = new Person();
console.log(person2.lessons); //Math,Physics,Biology,person1修改影响了person2

组合构造函数及原型模式

目前最为常用的定义类型方式,是组合构造函数模式与原型模式。构造函数模式用于定义实例的属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方方法的引用,最大限度的节约内存。此外,组合模式还支持向构造函数传递参数,可谓是集两家之所长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Shelby', 'Court'];
}
Person.prototype = {
constructor: Person, //原型字面量方式会将对象的constructor变为Object,此外强制指回Person
sayName: function(){
console.log(this.name);
}
};

var person1 = new Person('Jack', 19, 'SoftWare Engneer');
var person2 = new Person('Lily', 39, 'Mechanical Engneer');

person1.friends.push('Van');

console.log(person1.friends); //["Shelby", "Court", "Van"]
console.log(person2.friends); //["Shelby", "Court"]

console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true 共享原型中定义方法

在所接触的JS库中,jQuery类型的封装就是使用组合模式来实例的!!!

动态原型模式

组合模式中实例属性与共享方法(由原型定义)是分离的,这与纯面向对象语言不太一致;动态原型模式将所有构造信息都封装在构造函数中,又保持了组合的优点。其原理就是通过判断构造函数的原型中是否已经定义了共享的方法或属性,如果没有则定义,否则不再执行定义过程。该方式只原型上方法或属性只定义一次,且将所有构造过程都封装在构造函数中,对原型所做的修改能立即体现所有实例中:

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.lessons = ['Math', 'Physics'];

}
if (typeof this.sayName != 'function') { //通过判断实例封装
  Person.prototype = {
    constructor: Person, //原型字面量方式会将对象的constructor变为Object,此外强制指回Person
    sayName: function() {
      return this.name;
    }
  }
}
var person1 = new Person('Jack', 19, 'SoftWare Engneer');
person1.lessons.push('Biology');
var person2 = new Person('Lily', 39, 'Mechanical Engneer');
console.log(person1.lessons); //Math,Physics,Biology
console.log(person2.lessons); //Math,Physics
console.log(person1.sayName === person2.sayName); //true,//共享原型中定义方法

以上内容参考自《Javascript高级程序设计》第3版

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2019 FE blog All Rights Reserved.

访客数 : | 访问量 :