javascript中的面向对象程序设计

面向对象

面向对象(Objet-Oriented,OO)的语言有一个标志,那就是他们都有类的概念,而通过类可以创建具有多个相同属性和方法的对象。ES中没有类的概念,ES中把对象定义为“无序属性的集合,其属性可以包含基本值,对象或者函数”,可以把ES中的对象想象成散列表:无非就是一组键值对,其中值可以是数据或函数。

创建对象

New Object方法与字面量方法

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

1
2
3
4
5
6
7
var person = new Object();
person.name = "wzb";
person.age = 22;
person.job = "learning";
person.sayName = function(){
alert(this.name);
}

使用对象字面量语法定义对象:

1
2
3
4
5
6
7
8
9
var person = {
name: "wzb";
age: 22;
job: "learning";

sayName: function(){
alert(this.name);
}
}

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

工厂模式

工厂模式抽象了创建具体对象的过程。考虑到无法创建类,开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。

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(){
alert(this.name);
}
return o;
}
var person1 = createPerson("wzb",22,"learning");
var person2 = createPerson("husbin",22,"learning");

构造函数模式

ECMAScript中的构造函数可以用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以自定义构造函数,从而定义自定义对象类型的属性和方法。(调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针。)例如,可以使用构造函数模式将前面的例子重写:

1
2
3
4
5
6
7
8
9
10
function Person(name , age , job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("wzb",22,"learning");
var person2 = new Person("husbin",22,"learning");
将构造函数当作函数

构造函数与其他函数的唯一区别,在于调用他们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那他就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那么它跟普通函数不会有什么区别。例如前面的Person例子:

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

//当作普通函数调用
Person("wzb",22,"learning");
window.sayName(); //wzb

//在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"wzb",22,"learning");
o.sayName(); //wzb
工厂模式与构造函数的不同之处
  • 构造函数没有显示创建对象
  • 直接将属性和方法付给了this对象
  • 没有return语句
New 操作符
  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象
构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,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)"); //与声明函数在逻辑上是等价的。
}

不同实例上的同名函数是不相等的,举个例子:

1
alert(person1.sayName === person2.sayNaem);		//false

然而,创建两个完全同样任务的Function实例时没有必要的,况且存在this对象,根本必用在执行代码前把函数绑定到特定的对象上面。因此,可以通过把函数定义转移到构造函数外部来解决这个问题。

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 = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("wzb",22,"learning");
var person2 = new Person("husbin",22,"learning");

新的缺点:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不其实。而且如果对象需要定义很多方法,那么就要定义多个全局函数,于是这个自定义的引用类型就丝毫没有封装性可言了。

原型模式

每个函数函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按字面意思来理解,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享他所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(){

}
Person.prototype.name = "wzb";
Person.prototype.age = 22;
Person.prototype.job = "learning";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName(); //wzb

var person2 = new Person();
person2.sayName(); //wzb
alert(person1.sayName === person2.sayName); //true

从上面的例子可以看出,构造函数是空函数,即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数。

更简单的原型语法
1
2
3
4
5
6
7
8
9
10
11
function Person(){
}
Person.pototype = {
constructor:Person,
name: "wzb",
age:22,
job:"learning",
sayName: function(){
alert(this.name);
}
}
原型对象的问题
  • 首先,他省略了为构造函数传递初始化参数这一环节,结果是所有实例在默认情况下都将取得相同的属性值,虽然这会在某种程度上带来一些不方便,但这不是原型模式的最大问题。原型模式的最大问题是由其共享的本性所导致的。原型中的所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值得属性倒也说得过去,毕竟,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(){

}
Person.prototype = {
name: "wzb",
age: 22,
job: "learning",
friends: ["baicai","donggua"],
sayName: function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();

person1.friends.push("husbin");

console.log(person1.friends); //["baicai", "donggua", "husbin"]
console.log(person2.friends); //["baicai", "donggua", "husbin"]
alert(person1.friends === person2.friends); //true;
  • 实例一般都是要有属于自己的全部属性,然而上面的所有实例却共享一个数组,这也是很少看到别人单独使用原型模式的原因。
组合使用构造函数模式和原型模式

创建自动逸类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数,可谓是集两种模式之长。举个例子:

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=["wzb","sb"];
}
Person.prototype={
constructor: Person,
sayName: function(){
alert(this.name);
}
}
var person1 = new Person("AAA",22,"learning");
var person2 = new Person("BBB",22,"learning");
person1.friends.push("ccc");
console.log(alert(person1.friends)); //wzb,sb,ccc
console.log(alert(person2.friends)); //wzb,sb
alert(person1.friends === person2.friends); //false
alert(person1.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("wzb",22,"learning");
friend.sayName(); //wzb
寄生构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,但从表面上看,这个函数又很像典型的构造函数。

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(){
alert(this.name);
}
return o;
}
var friend = new Person("wzb",22,"learning");
friend.sayName();

继承

许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript中无法实现接口继承。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;
}

//继承了SuperType
SubType.prototype = new SuperType();

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

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

原型搜索机制

当以读取模式访问一个实例属性时,首先会在实例中搜索该属性,如果没有找到该属性,则会继续搜索实例的原型,在通过原型链实现集成的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说:调用instance.getSuperTypeValue()会经历三个搜索步骤:①搜索实例;②搜索SubType.prototype;③搜索SuperType.prototype,最后才找到该方法。

默认的原型

所有引用类型默认都继承了Object,而这个继承也是通过原型链来实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。

确定原型和实例的关系

可以通过两种方式来确定原型和实例的关系。第一种是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,就会返回true。

1
2
3
alert(instance instanceof Object);		//true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true

第二种是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。因此isPrototypeOf()方法也会返回true。

1
2
3
alert(Object.prototype.isPrototypeOf(instance));	//true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

原型链的问题

最主要的问题是来自包含引用类型值的原型,原先实力属性会变成现在的原型属性。

第二个问题是在创建子类型的实例时,不能向超类型的构造函数中传递参数。

举个例子:

1
2
3
4
5
6
7
8
9
10
function SuperType(){
this.color=["red","blue","green"];
}
function SubType(){}
SubType.prototype = new SuperType();
var instance = new SubType();
instance.color.push("black");
console.log(instance.color); // ["red", "blue", "green", "black"]
var instance1 = new SubType();
console.log(instance1.color); //["red", "blue", "green", "black"]

借用构造函数

也叫伪造对象或经典继承。这种技术的基本思想很简单,即在子类型的构造函数的内部调用超类型的构造函数。函数只不过是在特定环境中执行代码的对象,因此可以通过apply()call()方法也可以在新创建的对象上执行构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType(){
this.color=["red","blue","green"];
}
function SubType(){
//继承了SuperType
SuperType.call(this); //借调了超类型的构造函数
}
SubType.prototype = new SuperType();
var instance = new SubType();
instance.color.push("black");
console.log(instance.color); //["red", "blue", "green", "black"]
var instance1 = new SubType();
console.log(instance1.color); //["red", "blue", "green"]

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

组合式继承

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

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
function SuperType(name){
this.name=name;
this.color=["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("wzb",22);
instance1.color.push("black");
instance1.sayName(); //wzb
instance1.sayAge(); //22
alert(instance1.color); //red,blue,green,black

var instance2 = new SubType("111",22);
instance2.sayName(); //111
instance2.sayAge(); //22
alert(instance2.color); //red,blue,green

在这个例子中,SuperType构造函数定义了2个属性:name和color。SuperType的原型定义了一个方法sayName(),SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了自己的属性age。然后,将SuperType的实例赋给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType实例既分别拥有自己的属性,又可以使用相同的方法了。