前言
博客园谈设计模式的文章很多,我也受益匪浅,包括TerryLee、吕震宇等等的.NET设计模式系列文章,强烈推荐。对于我,擅长于前台代码的开发,对于设计模式也有一定的了解,于是我想结合Javascript来设计前台方面的“设计模式”,以对后台“设计模式”做个补充。开始这个系列我也诚惶诚恐,怕自己写得不好,不过我也想做个尝试,一来希望能给一些人有些帮助吧,二来从写文章中锻炼下自己,三来通过写文章对自己增加自信;如果写得不好,欢迎拍砖,我会虚心向博客园高手牛人们学习请教;如果觉得写得还可以,谢谢大家的支持了:)
在开始设计模式的书写之前,有必要对Javascript面向对象的概念先做个介绍,那么这篇文章就以面向对象基础作为起点吧。
那好了,闲话不多说了,开始这系列的开篇文章
理论知识
1. 首先Javascript是弱类型语言,它定义变量时不必声明类型,如var Person = new Person(),它的变量类型为“var”,现在的C# 3.0也引进了这种匿名类型的概念,弱类型的变量产生了极大的灵活性,因为Javascript会根据需要来进行类型转换。所以这也决定了它采用了晚绑定的方法,即在运行后才知道变量的类型;
2. 面向对象概念不必多说,封装,继承,多态;
3. Javascript对象的类型主要分为三种:本地对象,如String,Array,Date等;内置对象,如Global,Math等;宿主对象,是指BOM,DOM对象等等;变量范围包括传统面向对象程序设计中的作用域,如公有,保护,私有,静态等等;
主要内容
1. 现在让我们来看看Javascript怎样创建对象的:
function
Man() {
//
}
Man.prototype.getNickName
=
function
() {
return
"
Leepy
"
;
};
var
man
=
new
Man();
var
name
=
man.getNickName();
这样就创建了最简单的类和对象,其中我们可以把function Man() {} 看作是Man类的构造函数,getNickName()看作是Man类的方法,准确说可以“当作”是Man类的公共方法;为什么要说是当作呢?那是因为其实Javascript实际上并没有一个私有共有的划分,因此开发者们自己指定了这样的规约,那么规约是什么样的呢?我这里把Man类的清单完整地列出来:
function
Man() {
//
私有静态属性
var
Sex
=
"
男
"
;
//
私有静态方法
function
checkSex() {
return
(Sex
==
"
男
"
);
}
//
私有方法
this
._getSex
=
function
() {
//
调用私有静态方法
if
(checkSex())
return
"
男
"
;
else
return
"
女
"
;
}
//
私有方法
this
.getFirstName
=
function
() {
return
"
Li
"
;
};
//
私有方法
this
.getLastName
=
function
() {
return
"
Ping
"
;
};
}
//
公共方法
Man.prototype.getNickName
=
function
() {
return
"
Leepy
"
;
};
//
公共方法
Man.prototype.getFullName
=
function
() {
return
this
.getFirstName()
+
"
"
+
this
.getLastName();
};
//
公共方法
Man.prototype.getSex
=
function
() {
//
调用私有方法
return
this
._getSex();
};
//
公共静态方法
Man.say
=
function
() {
return
"
Happy new year!
"
;
}
这样的类是否看起来和传统的类很相似了呢^_^
2. 接下来这个是本篇的一个重点,就是用Javascript如何设计一个接口,然后让类继承于它。
首先,先让我们看传统的C#语言是如何设计接口的吧:
public
interface
Person
{
string
GetName();
void
SetName(
string
name);
}
public
class
Man : Person
{
private
string
_name;
public
string
GetName()
{
return
_name;
}
public
void
SetName(
string
name)
{
_name
=
name;
}
}
接口中可以声明属性、方法、事件和类型(Structure),(但不能声明变量),但是并不能设置这些成员的具体值,也就是说,只能定义,不能给它里面定义的东西赋值,而接口作为它的继承类或者派生类的规约,继承类或者它的派生类能够共同完成接口属性、方法、事件和类型的具体实现,因为这里GetName(),SetName(),不管是方法名还是属性调用顺序上都是要保持一致的;
那么有了这样的一个基于接口的思想,我们设计Javascript的接口类的时候也需要考虑到这个规范。我先从主JS文件调用端开始说起:
var
Person
=
new
Interface(
"
Person
"
, [[
"
getName
"
,
0
], [
"
setName
"
,
1
]]);
其中Interface类是稍后要说的接口类,第一个参数"Person"是接口类的名称,第二个参数是个二维数组,"getName"是接口方法的名称,"0"是该方法所带的参数个数(因为Javascript是弱语言,所以类型是不确定的,所以只要记住参数个数就好,"0"可以省略不写),"setName"同理。这样一个接口定义好了。怎样使用它呢?
function
Man()
{
this
.name
=
""
;
Interface.registerImplements(this, Person);
}
Man.prototype.getName
=
function
() {
return
this
.name;
};
Man.prototype.setName
=
function
(name) {
this
.name
=
name;
};
看到Man的构造函数里面包含
Interface.registerImplements(this, Person);
它是用来将实例化的this对象继承于Person接口,然后继承类对接口的方法进行实现。
代码看起来是不是很清晰和简单呢^_^,那么现在要开始介绍真正的核心代码Interface.js了:
先看Interface的构造函数部分
function
Interface(name, methods)
{
if
(arguments.length
!=
2
) {
throw
new
Error(
"
接口构造函数含
"
+
arguments.length
+
"
个参数, 但需要2个参数.
"
);
}
this
.name
=
name;
this
.methods
=
[];
if
(methods.length
<
1
) {
throw
new
Error(
"
第二个参数为空数组.
"
);
}
for
(
var
i
=
0
, len
=
methods.length; i
<
len; i
++
) {
if
(
typeof
methods[i][
0
]
!==
'
string
'
) {
throw
new
Error(
"
接口构造函数第一个参数必须为字符串类型.
"
);
}
if
(methods[i][
1
]
&&
typeof
methods[i][
1
]
!==
'
number
'
) {
throw
new
Error(
"
接口构造函数第二个参数必须为整数类型.
"
);
}
if
(methods[i].length
==
1
) {
methods[i][
1
]
=
0
;
}
this
.methods.push(methods[i]);
}
};
刚才看到了var Person = new Interface("Person", [["getName", 0], ["setName", 1]]);,这里将两个参数分别保存起来;
调用方法部分:
Interface.registerImplements
=
function
(object) {
if
(arguments.length
<
2
) {
throw
new
Error(
"
接口的实现必须包含至少2个参数.
"
);
}
for
(
var
i
=
1
, len
=
arguments.length; i
<
len; i
++
) {
var
interface
=
arguments[i];
if
(interface.constructor
!==
Interface) {
throw
new
Error(
"
从第2个以上的参数必须为接口实例.
"
);
}
for
(
var
j
=
0
, methodsLen
=
interface.methods.length; j
<
methodsLen; j
++
) {
var
method
=
interface.methods[j][
0
];
if
(
!
object[method]
||
typeof
object[method]
!==
'
function
'
||
object[method].getParameters().length
!=
interface.methods[j][
1
]) {
throw
new
Error(
"
接口的实现对象不能执行
"
+
interface.name
+
"
的接口方法
"
+
method
+
"
,因为它找不到或者不匹配.
"
);
}
}
}
};
刚才这句Interface.registerImplements(this, Person);,实际上这里是把this对象的方法名以及参数个数与刚Person保存的methods逐一进行比较,如果找不到或者不匹配,就警告错误;其中object[method].getParameters().length,调用了如下的代码:
Function.prototype.getParameters
=
function
() {
var
str
=
this
.toString();
var
paramString
=
str.slice(str.indexOf(
'
(
'
)
+
1
, str.indexOf(
'
)
'
)).replace(
/
\s*
/
g,
''
);
//
取得参数字符串
try
{
return
(paramString.length
==
0
?
[] : paramString.split(
'
,
'
));
}
catch
(err)
{
throw
new
Error(
"
函数不合法!
"
);
}
}
getParrameters()方法作为Function对象的一个扩展,功能是取得方法含有的参数数组;
Interface.js完整的代码如下:
Interface.js文件
function Interface(name, methods)
{
if(arguments.length != 2) {
throw new Error("接口构造函数含" + arguments.length + "个参数, 但需要2个参数.");
}
this.name = name;
this.methods = [];
if(methods.length < 1) {
throw new Error("第二个参数为空数组.");
}
for(var i = 0, len = methods.length; i < len; i++) {
if(typeof methods[i][0] !== 'string') {
throw new Error("接口构造函数第一个参数必须为字符串类型.");
}
if(methods[i][1] && typeof methods[i][1] !== 'number') {
throw new Error("接口构造函数第二个参数必须为整数类型.");
}
if(methods[i].length == 1) {
methods[i][1] = 0;
}
this.methods.push(methods[i]);
}
};
Interface.registerImplements = function(object) {
if(arguments.length < 2) {
throw new Error("接口的实现必须包含至少2个参数.");
}
for(var i = 1, len = arguments.length; i < len; i++) {
var interface = arguments[i];
if(interface.constructor !== Interface) {
throw new Error("从第2个以上的参数必须为接口实例.");
}
for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
var method = interface.methods[j][0];
if(!object[method] || typeof object[method] !== 'function' || object[method].getParameters().length != interface.methods[j][1]) {
throw new Error("接口的实现对象不能执行" + interface.name + "的接口方法" + method + ",因为它找不到或者不匹配.");
}
}
}
};
Function.prototype.getParameters = function() {
var str = this.toString();
var paramString = str.slice(str.indexOf('(') + 1, str.indexOf(')')).replace(/\s*/g,''); //取得参数字符串
try
{
return (paramString.length == 0 ? [] : paramString.split(','));
}
catch(err)
{
throw new Error("函数不合法!");
}
}
好了该创建一个html页面来试试效果了:
<
script
type
="text/javascript"
>
function
test()
{
var
man
=
new
Man();
man.setName(
"
Leepy
"
);
alert(man.getName());
}
</
script
>
<
input
type
="button"
value
="click"
onclick
="test();"
/>
最终结果为:"Leepy"的弹出框;
这里还有一点要强调,如果接口上的方法没有在继承类上得到完全实现,或者方法参数个数不匹配,那么就会提示错误;
3. 如果我要一个类继承于另一个类该怎么做呢,继续看例子,这里我再定义一个SchoolBoy(男学生)类:
function
SchoolBoy(classNo, post)
{
Man.call(
this
);
this
._chassNo
=
classNo;
this
._post
=
post;
}
SchoolBoy.prototype
=
new
Man();
SchoolBoy.prototype.getName
=
function
() {
return
"
Mr
"
+
this
.name;
}
SchoolBoy.prototype.setName
=
function
(name) {
this
.name
=
name
+
"
's
"
;
}
其中Man.call(this);实际上是将Man中的关键字this赋值于SchoolBoy对象中去,那么SchoolBoy就拥有了Man构造函数中的name属性了;
SchoolBoy.prototype = new Man();实际上是把Man的prototype赋值给SchoolBoy.prototype,那么SchoolBoy就有了Man类中的方法;
而后面跟着的getName(),setName(),实际上是覆盖了前面继承于Man类中的方法了;
然后看看效果:
var
schoolboy
=
new
SchoolBoy(
"
三年二班
"
,
"
班长
"
);
schoolboy.setName(
"
周杰伦
"
);
alert(schoolboy.getName());
最后结果为:"Mr 周杰伦's"的弹出框;
总结
该篇文章主要讲述一些Javascript面向对象的基础以及接口和继承类的实现等等;下一篇将开始真正的Javascript设计模式征程,并且我会结合一些网站系统上的实例来讲;
本篇到此为止,谢谢大家阅读!
本系列文章转载时请注明出处,谢谢合作!
相关系列文章:
Javascript乱弹设计模式系列(6) - 单件模式(Singleton)
Javascript乱弹设计模式系列(5) - 命令模式(Command)
Javascript乱弹设计模式系列(4) - 组合模式(Composite)
Javascript乱弹设计模式系列(3) - 装饰者模式(Decorator)
Javascript乱弹设计模式系列(2) - 抽象工厂以及工厂方法模式(Factory)
Javascript乱弹设计模式系列(1) - 观察者模式(Observer)
Javascript乱弹设计模式系列(0) - 面向对象基础以及接口和继承类的实现