本文为书籍《Professional JavaScript for Web Developers, 3rd Edition》英文版第 6 章:“Object-Oriented Programming” 个人学习总结,主要介绍 JavaScript 中自定义类型的产生和类型继承实现的各种模式及其优缺点。
一.
类和对象的产生
本节介绍在
10
种在
JavaScript
中产生对象的模式及其优缺点。
1.Object构造函数模式
var
person
=
new
Object
();
person
.
name
=
"Nicholas"
;
person
.
age
=
29
;
或
var
person
=
{};
person
.
name
=
"Nicholas"
;
person
.
age
=
29
;
2.对象字面表达(object literal)
模式
// 这是定义对象较好的一种方式,书写方便,简洁易读
var
person
=
{
// 属性和属性的值用冒号分开
name
:
"Nicholas"
,
// 用逗号分开属性定义
age
:
29
// 最后定义的属性不需要逗号
};
// 结尾最好带分号
可以使用字符串和数字作为属性名称,如:
var
person
=
{
"name"
:
"Nicholas"
,
"age"
:
29
,
5
:
true
// 为数字的属性名称将自动转为字符串
};
3.工厂模式
前两种创建对象的方式简单,但如果要创建具有相同类型的多个对象,
只能重复相同的代码。也就是说,无法创建一种类型的多个对象实例
。所以这两种方法只能产生一个对象,它们的类型始终是Object,没有
具体的类型。
即:var
person1
=
new
person
()
或var
person1
=
new
person是无
效的。
工厂模式可以解决使用一种方式创建多个相同类型对象的问题,如:
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
(
"Nicholas"
,
29
,
"Software Engineer"
);
var
person2
=
createPerson
(
"Greg"
,
27
,
"Doctor"
);
工厂模式解决了对象的产生问题,但是工厂模式产生的对象仍不能
判断具体的类型。比如上例中,person1不能被判断是否为Person类型。
4.构造函数模式
function
Person
(
name
,
age
,
job
)
{
this
.
name
=
name
;
//this关键字使得类型
this
.
age
=
age
;
//的成员能够被外界访问
this
.
job
=
job
;
this
.
sayName
=
function
()
{
alert
(
this
.
name
);
};
}
var
person1
=
new
Person
(
"Nicholas"
,
29
,
"Software Engineer"
);
var
person2
=
new
Person
(
"Greg"
,
27
,
"Doctor"
);
由于JavaScript中函数的性质,在产生类实例时可以不用传递全部
参数,甚至不传递参数,如:
var
person1
=
new
Person
()
var
person2
=
new
Person
(
"Jim"
);
var
person3
=
new
Person
(
"Tom"
,
29
);
person1
.
sayName
();
//undefined
person1
.
name
=
"Tim"
;
person1
.
sayName
();
//"Tim"
person2
.
sayName
();
//"Jim"
person3
.
sayName
();
//"Tom"
构造函数模式能够很好地产生一种类型,并创建这种类型的多个
对象实例。比如,我们根据面向对象的概念,可以认为
Person
是
一个类,而
person1
和
person2
是
Person
的两个实例对象。
可以判断构造函数产生的对象实例的类型,有两种方法:
// constructor 属性是每个引用类型对象都具有的属性
// 指向定义对象的构造函数
alert
(
person1
.
constructor
==
Person
);
//true
alert
(
person2
.
constructor
==
Person
);
//true
或
alert
(
person1
instanceof
Object
);
//true
alert
(
person1
instanceof
Person
);
//true
alert
(
person2
instanceof
Object
);
//true
alert
(
person2
instanceof
Person
);
//true
(
1
)
是类,也是函数
构造函数模式建立的类是使用函数方式定义的,因此这种类型
既是类也是函数。下面的例子说明了这一点
:
//use as a constructor
var
person
=
new
Person
(
"Nicholas"
,
29
,
"Software Engineer"
);
person
.
sayName
();
//"Nicholas"
//call as a function
Person
(
"Greg"
,
27
,
"Doctor"
);
//adds to window
window
.
sayName
();
//"Greg"
//call in the scope of another object
var
o
=
new
Object
();
Person
.
call
(
o
,
"Kristen"
,
25
,
"Nurse"
);
o
.
sayName
();
//"Kristen"
(
2
)
构造函数模式的问题
在JavaScript中,函数即对象,所以在类内部定义的方法,当实
例化类创建多个对象时,每个对象内的方法虽然具有相同的名称,
但却是不同的实例。所以构造函数模式产生的对象实例不能重用方法
功能,只能各自产生新的方法实例,造成代码冗余。可以将方法的定
义放到类的外部来解决这个问题。如:
function
Person
(
name
,
age
,
job
)
{
this
.
name
=
name
;
this
.
age
=
age
;
this
.
job
=
job
;
this
.
sayName
=
sayName
;
}
function
sayName
()
{
alert
(
this
.
name
);
}
这样,sayName
()
的功能便可以重用。但这样不利于代码组织,容
易产生混乱的代码。sayName
()
函数本来只应属于类Person,却在全局
作用域中定义,可以在其它类、对象和函数等作用域内调用。这些问
题可以使用原型
(
Prototype
)
模式解决。
5.原型(Prototype)
模式
所有的函数都具有一个
prototype
属性,该属性对于引用类型的
实例都可用,是一个包含属性与方法的对象。每当通过构造函数产生
对象时,将以
prototype
作为原型产生对象。
prototype
的所有属性
和方法将在所有实例对象间共享。属性默认值在各实例对应属性中是
相等的,每个对象可以重新定义属性的值,但重新定义的属性
(
与
prototype
中属性名称相同
)
仅位于每个对象
(
实例
)
上,
prototype
中
的对应属性被屏蔽,但仍可以通过
prototype
属性访问。对象的信息可
以在构造函数中定义,也可以在
prototype
对象上定义。例如:
function
Person
()
{
}
Person
.
prototype
.
name
=
"Nicholas"
;
Person
.
prototype
.
age
=
29
;
Person
.
prototype
.
job
=
"Software Engineer"
;
Person
.
prototype
.
sayName
=
function
()
{
alert
(
this
.
name
);
};
var
person1
=
new
Person
();
//person1.sayName(); //"Nicholas"
var
person2
=
new
Person
();
//person2.sayName(); //"Nicholas"
//alert(person1.sayName == person2.sayName); //true
//alert(person1 instanceof Object); //true
//alert(person1 instanceof Person); //true
//alert(person2 instanceof Object); //true
//alert(person2 instanceof Person); //true
//name属性被重新定义在person1上,
//prototype中的name属性被屏蔽
person1
.
name
=
"Jim"
;
person2
.
name
=
"Tom"
;
//person1.sayName(); //"Jim" -- 来自实例
//person2.sayName(); //"Tom"
alert
(
person1
.
name
);
//"Jim" -- 来自实例
//"Nicholas" -- 来自 prototype
alert
(
person1
.
constructor
.
prototype
.
name
);
var
person3
=
new
Person
();
person3
.
sayName
()
//"Nicholas" -- 来自 prototype
也可以这样使用原型模式:
function
Person
()
{
}
//使用对象方式定义原型,默认的prototype对象属性被覆写
Person
.
prototype
=
{
name
:
"Nicholas"
,
age
:
29
,
job
:
"Software Engineer"
,
sayName
:
function
()
{
alert
(
this
.
name
);
}
};
这种原型模式与上一个例子中的大致相同,但是所产生实例的
constructor
属性不再指向初始的构造函数
(
本例中为Person
)
,每个
实例的
constructor
和
prototype
都会重新产生,覆写默认的
constructor
和
prototype
。
instanceof
运算符作用于每个实例
仍可以可靠地工作,constructor
却不可靠,如:
var
friend
=
new
Person
();
alert
(
friend
instanceof
Object
);
//true
alert
(
friend
instanceof
Person
);
//true
alert
(
friend
.
constructor
==
Person
);
//false
alert
(
friend
.
constructor
==
Object
);
//true
原因是,
prototype
对象也具有自己的
constructor
属性,
默认指向
prototype
所在的构造函数,但是采用上面的原型模式
所产生的对象会覆写
prototype
的
constructor
属性所指向的构
造函数。
这个问题可以通过以下方法解决:
function
Person
()
{
}
Person
.
prototype
=
{
constructor
:
Person
,
//设置 prototype 的构造函数
name
:
"Nicholas"
,
age
:
29
,
job
:
"Software Engineer"
,
sayName
:
function
()
{
alert
(
this
.
name
);
}
};
需要注意的是,使用对象覆写默认原型时,实例化应在定义原型
之后,否则可能得不到预期效果。如:
function
Person
()
{
}
var
friend
=
new
Person
();
Person
.
prototype
=
{
constructor
:
Person
,
name
:
"Nicholas"
,
age
:
29
,
job
:
"Software Engineer"
,
sayName
:
function
()
{
alert
(
this
.
name
);
}
};
friend
.
sayName
();
//error
这里,Person的实例
friend
产生在
prototype
对象被覆写之前,
friend仍指向默认的
prototype
,但默认的
prototype
中没有
sayName
()
方法,因此产生错误。如果
friend
产生在
prototype
对象
被覆写之后,则
friend
将指向新的
prototype
,不会出错。
可以通过
prototype
扩展或覆写像
window
,
string
,
math等预定义
对象的属性和方法。
(
1
)
原型模式的问题
原型模式创建对象无法通过构造函数传递初始化参数,并且,若在
原型上定义的属性是引用类型,则每个实例相同名称的引用类型的属性
总是具有相同的值,这可以实现静态成员,但若每个实例想拥有各自的
引用类型的属性值,这就成了问题。因此,纯粹的原型模式也很少用。
6.构造函数/
原型模式
这种模式属于构造函数模式和原型模式的混合。实例属性在构造函数
中定义,方法和共享属性在原型中定义。这种模式很好地综合了构造
函数模式和原型模式的优点,同时解决了各自的问题。如:
function
Person
(
name
,
age
,
job
)
{
this
.
name
=
name
;
this
.
age
=
age
;
this
.
job
=
job
;
this
.
friends
=
[
"Shelby"
,
"Court"
];
}
Person
.
prototype
=
{
constructor
:
Person
,
sayName
:
function
()
{
alert
(
this
.
name
);
}
};
var
person1
=
new
Person
(
"Nicholas"
,
29
,
"Software Engineer"
);
var
person2
=
new
Person
(
"Greg"
,
27
,
"Doctor"
);
person1
.
friends
.
push
(
"Van"
);
alert
(
person1
.
friends
);
//"Shelby,Court,Van"
alert
(
person2
.
friends
);
//"Shelby,Court"
alert
(
person1
.
friends
===
person2
.
friends
);
//false
alert
(
person1
.
sayName
===
person2
.
sayName
);
//true
7.动态原型模式
这种模式将原型定义放在构造函数内部,必要时可以检查方法是否
可用以决定是否需要初始化原型。如:
function
Person
(
name
,
age
,
job
)
{
//properties
this
.
name
=
name
;
this
.
age
=
age
;
this
.
job
=
job
;
//methods
//if (typeof this.sayName != "function")
//{
Person
.
prototype
.
sayName
=
function
()
{
alert
(
this
.
name
);
};
//}
}
var
friend
=
new
Person
(
"Nicholas"
,
29
,
"Software Engineer"
);
friend
.
sayName
();
可以通过
if
语句检查任何属性或方法,但没有必要使用多个
if
语
句,检查任何一个属性或方法即可。这种模式具有以上各种模式的优点
,同时便于代码组织。
8.寄生(Parasitic)
构造函数模式
这种模式类似于工厂模式的定义,但创建对象时,使用
new
关键字。
例如:
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
(
"Nicholas"
,
29
,
"Software Engineer"
);
friend
.
sayName
();
//"Nicholas"
alert
(
friend
instanceof
Person
);
//false
这种模式产生的对象与构造函数无关,也不能依赖
instanceof
运算符
判断对象的类型,因此,仅在特殊情况下使用该模式。
9.持久(Durable)
构造函数模式
这种模式创建的对象没有公共属性,方法也不引用
this
对象。此模式
常应用于安全环境和混搭模式,不允许使用
this
和
new。如:
function
Person
(
name
,
age
,
job
)
{
//create the object to return
var
o
=
new
Object
();
//optional: define private variables/functions here
//attach methods
o
.
sayName
=
function
()
{
alert
(
name
);
};
//return the object
return
o
;
}
var
friend
=
Person
(
"Nicholas"
,
29
,
"Software Engineer"
);
friend
.
sayName
();
//"Nicholas"
类似于寄生函数模式,不能依赖
instansof
运算符判断对象类型。
10.ECMAScript 6
模式
在
ECMAScript
6
中引入了许多面向对象语言的特性,在实现了
ECMAScript
6
的
JavaScript中,可以像许多
C
语言风格的面向对象
编程语言一样定义类。例如,一个常规的
JavaScript
自定义类型如下:
function
Person
(
name
,
age
)
{
this
.
name
=
name
;
this
.
age
=
age
;
}
Person
.
prototype
.
sayName
=
function
()
{
alert
(
this
.
name
);
};
Person
.
prototype
.
getOlder
=
function
(
years
)
{
this
.
age
+=
years
;
};
如果使用实现了
ECMAScript
6
的
JavaScript来定义与上例相同的
类型,可以这样:
class
Person
{
constructor
(
name
,
age
)
{
public
name
=
name
;
public
age
=
age
;
}
sayName
()
{
alert
(
this
.
name
);
}
getOlder
(
years
)
{
this
.
age
+=
years
;
}
}
但是由于目前浏览器的兼容性原因,ECMAScript
6
定义类型的模式
也不常用。所以这里不做详细介绍。
为了使
JavaScript
更好地以
ECMAScript
6
方式工作,同时解决浏
览器兼容性问题,微软推出了开源的
TypeScript,其官方网址是:
http
:
//www.typescriptlang.org/
二.
继承
JavaScript中,主要是通过原型链
(
Prototype
Chaining
)
来实现继承,
并且只有实现继承,没有接口继承。
1.原型链继承
JavaScript中对象的
prototype
属性一个很重要的方面就是用于实现
类型的继承。所有引用类型的对象默认都继承了
Object
对象的
prototype
,因而都有
toString
()
和
valueOf
()
等方法。
原型链继承的模式如下:
// 父类
function
SuperType
()
{
this
.
property
=
true
;
}
SuperType
.
prototype
.
getSuperValue
=
function
()
{
return
this
.
property
;
};
// 子类
function
SubType
()
{
this
.
subproperty
=
false
;
}
// 使 SubType 从 SuperType 继承
// 这是原型链继承的关键
SubType
.
prototype
=
new
SuperType
();
// 在子类的原型上定义新的子类的方法
SubType
.
prototype
.
getSubValue
=
function
()
{
return
this
.
subproperty
;
};
var
instance
=
new
SubType
();
alert
(
instance
.
getSuperValue
());
//true
有两种方法用于判定实例与类型的关系,例如,对于上面的代码:
//使用 instanceof 运算符
alert
(
instance
instanceof
Object
);
//true
alert
(
instance
instanceof
SuperType
);
//true
alert
(
instance
instanceof
SubType
);
//true
//使用类型prototype属性的isPrototypeOf方法
alert
(
Object
.
prototype
.
isPrototypeOf
(
instance
));
//true
alert
(
SuperType
.
prototype
.
isPrototypeOf
(
instance
));
//true
alert
(
SubType
.
prototype
.
isPrototypeOf
(
instance
));
//true
如果子类要重写父类的方法,可以在将父类的实例赋值给子类的
原型后,在子类的原型上定义相同名称的方法,如:
// ...
// 实现继承
SubType
.
prototype
=
new
SuperType
();
//重写继承的方法
SubType
.
prototype
.
getSuperValue
=
function
()
{
return
false
;
};
var
instance
=
new
SubType
();
alert
(
instance
.
getSuperValue
());
//false
需要注意的是,在原型链继承模式中,不可以在原型
(
prototype
)
上
用对象字面表示
(
object
literal
)
方式定义方法,这样会覆写已继承的
原型链。如:
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
;
},
someOtherMethod
:
function
()
{
return
false
;
}
};
var
instance
=
new
SubType
();
alert
(
instance
.
getSuperValue
());
// 错误!
原型链继承模式的问题
因为原型链继承模式是通过将父类的实例赋予子类,因而具有与原型
模式所产生的对象的同样的问题。如果父类中的某个属性是引用类型,则
子类的所有实例的这个属性的值都相同,修改子类一个实例的该属性的值
将会同时改变其他实例该属性的值。如:
// 父类
function
SuperType
()
{
this
.
colors
=
[
"red"
,
"blue"
,
"green"
];
}
// 子类
function
SubType
()
{
}
//从父类继承
SubType
.
prototype
=
new
SuperType
();
var
instance1
=
new
SubType
();
instance1
.
colors
.
push
(
"black"
);
alert
(
instance1
.
colors
);
//"red,blue,green,black"
var
instance2
=
new
SubType
();
alert
(
instance2
.
colors
);
//"red,blue,green,black"
上例中,子类
SubType
从父类
SuperType
继承了一个数组类型
(
引用类型
)
的属性
colors。在向
SubType
的实例
instance1
的
colors
属性中添加一个新项
black
后,SubType
的实例
instance2
中的
colors
属性也自动地拥有这个新项。这在很多情况下是不希望
看到的。
原型链继承模式的另一个问题是,当产生子类的新实例时,无法
向父类的构造函数传递参数。
由于以上两个原因,在实际中,很少单纯地使用原型链模式进行
继承。
2.构造函数窃取(Constructor Stealing)
模式继承
可以使用构造函数窃取继承模式解决原型
(
prototypes
)
上引用值
的继承问题,其基本思想是:在子类的构造函数内部调用父类的
apply
()
或
call
()
方法并传递
this
参数执行父类的构造函数,这
样,每个子类的实例从父类中继承的引用属性都是各自独立的,不受
其它实例的影响。如:
function
SuperType
()
{
this
.
colors
=
[
"red"
,
"blue"
,
"green"
];
}
function
SubType
()
{
//从父类 SuperType 继承
SuperType
.
call
(
this
);
}
var
instance1
=
new
SubType
();
instance1
.
colors
.
push
(
"black"
);
alert
(
instance1
.
colors
);
//"red,blue,green,black"
var
instance2
=
new
SubType
();
alert
(
instance2
.
colors
);
//"red,blue,green"
传递参数
构造函数窃取继承模式可以在子类的构造函数内向父类的构造函
数传递参数,这正好克服了原型链继承模式的相应缺点。例如:
function
SuperType
(
name
)
{
this
.
name
=
name
;
}
function
SubType
()
{
//从父类 SuperType 继承并传递参数
SuperType
.
call
(
this
,
"Nicholas"
);
//实例属性
this
.
age
=
29
;
}
var
instance
=
new
SubType
();
alert
(
instance
.
name
);
//"Nicholas";
alert
(
instance
.
age
);
//29
构造函数继承模式的问题
构造函数继承模式的问题具有与使用构造函数模式产生自定义类型
相似的问题:由于方法定义在构造函数内,不能进行功能重用。而且,
定义在父类原型上的方法不能被子类访问。因此,实际中,很少纯粹使
用构造函数窃取继承模式。
3.混合继承(Combination inheritance
)
这种继承方式综合了原型链
(
Prototype
Chaining
)
和构造函数窃取
(
Constructor
Stealing
)
各自的优点,并克服了相应的缺点。其基本思
想是:使用原型链继承在原型上的成员,使用构造函数窃取继承实例
属性。这允许重用定义在原型上的功能并允许各个实例拥有自己的属
性。这是JavaScript中最常用的继承方式,并能通过
instanceof
和
isPrototypeOf
()
区分对象的类型和组成。例如:
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
);
//第一次调用SuperType
this
.
age
=
age
;
}
SubType
.
prototype
=
new
SuperType
();
//第二次调用SuperType
SubType
.
prototype
.
constructor
=
SubType
;
SubType
.
prototype
.
sayAge
=
function
()
{
alert
(
this
.
age
);
};
var
instance1
=
new
SubType
(
"Nicholas"
,
29
);
instance1
.
colors
.
push
(
"black"
);
alert
(
instance1
.
colors
);
//"red,blue,green,black"
instance1
.
sayName
();
//"Nicholas";
instance1
.
sayAge
();
//29
alert
(
instance1
instanceof
SuperType
);
//true
alert
(
instance1
instanceof
SubType
);
//true
//alert(instance1.constructor);
var
instance2
=
new
SubType
(
"Greg"
,
27
);
alert
(
instance2
.
colors
);
//"red,blue,green"
instance2
.
sayName
();
//"Greg";
instance2
.
sayAge
();
//27
//alert(instance2 instanceof SuperType);
//alert(instance2.constructor);
上例中,SuperType的原型成员可以在SuperType函数内部定义,但
SubType的原型和原型成员却不可以在SubType内定义。如果要在SubType
内定义原型和原型上的成员,在instance2之前必须要有一个实例
(
例如
instance1
)
,instance2的所有成员以及继承成员都可以访问,但instance1
原型成员以及继承的原型成员都不可以访问。
混合模式因为调用两次SuperType,所以效率不是很高。
4.寄生混合继承
寄生混合继承使用构造函数窃取继承属性,使用原型链混合形式继承
方法。实质上是,使用寄生继承模式继承父类的原型然后将结果分配给
子类的原型。例如:
function
object
(
o
)
{
function
F
()
{
}
F
.
prototype
=
o
;
return
new
F
();
}
function
inheritPrototype
(
subType
,
superType
)
{
var
prototype
=
object
(
superType
.
prototype
);
//create object
prototype
.
constructor
=
subType
;
//augment object
subType
.
prototype
=
prototype
;
//assign object
}
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
;
}
inheritPrototype
(
SubType
,
SuperType
);
SubType
.
prototype
.
sayAge
=
function
()
{
alert
(
this
.
age
);
};
var
instance1
=
new
SubType
(
"Nicholas"
,
29
);
instance1
.
colors
.
push
(
"black"
);
alert
(
instance1
.
colors
);
//"red,blue,green,black"
instance1
.
sayName
();
//"Nicholas";
instance1
.
sayAge
();
//29
alert
(
instance1
instanceof
SuperType
);
var
instance2
=
new
SubType
(
"Greg"
,
27
);
alert
(
instance2
.
colors
);
//"red,blue,green"
instance2
.
sayName
();
//"Greg";
instance2
.
sayAge
();
//27
寄生混合继承比混合继承效率高,原型链也保持了完整,而且,
instanceof
和
isPrototypeOf
()
也能很好地工作,因而是一种最佳
的引用类型继承模式。其缺点是,代码组织不方便。