- 浏览: 1658463 次
- 性别:
文章分类
- 全部博客 (2929)
- 非技术 (18)
- Eclipse (11)
- JAVA (31)
- 正则表达式 (0)
- J2EE (4)
- DOS命令 (2)
- WEB前端 (52)
- JavaScript (69)
- 数据库 (8)
- 设计模式 (0)
- JFreechart (1)
- 操作系统 (1)
- 互联网 (10)
- EasyMock (1)
- jQuery (5)
- Struts2 (12)
- Spring (24)
- 浏览器 (16)
- OGNL (1)
- WebService (12)
- OSGi (14)
- 软件 (10)
- Tomcat (2)
- Ext (3)
- SiteMesh (2)
- 开源软件 (2)
- Hibernate (2)
- Quartz (6)
- iBatis (2)
最新评论
解开JavaScript生命的达芬奇密码
——如何使用JavaScript进行可靠的继承调用
作者:cleverpig
提出问题:
几
乎每位在开发JavaScript时尝试应用面向对象技术的开发者,或多或少都会问自己一个问题:“如何调用父类(super
class)的方法?”在Ajax技术还没有目前这样炙手可热之前,这种问题很少出现,因为大多数开发者仅在进行客户端form验证或者简单的
DHTML/DOM操作时使用JavaScript。在那些简单的解决方案中,函数式编程(functional programming)
是
很有意义的,面向对象编程则处在次之重要的位置。现在,Ajax技术发展势头迅猛,开发者已经建立了一个调用大量客户端JavaScript、不断增长
的、复杂的系统。因此,在JavaScript上尝试OO技术便成为了管理复杂性的一种手段。在此过程中,多数开发者很快便认识到:JavaScript
是一种原型化的(prototypical)语言,它缺少OO自身带来的多种便利。
OO设计的主旨和关于它的一些话题谈
起来很大,但只着眼于Class的定义方式,我认为它是JavaScript开发者尝试解决问题的首选。因此,你可以在互联网上找到许多不同的问题解决案
例,但在我看过它们后不免有些失望——这些案例都是在某个场合下适用,而不是放之四海而皆准的通法。而我对这个话题的兴趣来自于我的team在开发ThinWire Ajax Framework
的影响。由于这个框架生成出对客户端代码的需求,才使我们“被迫”去实现可靠的、支持父类方法调用的OO模式。通过父类调用,你可以进一步依靠类的继承特性来核心化通用代码,从而更易于减少重复代码,去掉客户端代码的坏味道
。
下面罗列出了一些在我的研究过程中遇到的解决方式。最终,我没有从中找出一个可以接收的解决方案,于是我不得不实现一个自己的解决方案,你将在本文的结尾部分看到这个方案。
然而父类调用在这里是最重要的OO机制,因此我需要一个相应的工作模式,也正是因为在我的观点中原型化方式是丑陋的,所以我更需要一种更加自然地使用JavaScript定义类的方法。
More Solutions:
好吧,让我们进入讨论。正如开发者所察觉的那样,在JS中实现基本的继承是很容易的事,事实上有一些众所周知的方法:
丑陋的Solution:
没有进行父类调用的简单继承:
// 提前写好的JavaScript Class定义和继承
// 当然,这种代码很丑陋,散发着代码的坏味道。
function BaseClass() {
//BaseClass constructor code goes here
}
BaseClass.prototype.getName = function() {
return "BaseClass";
}
function SubClass() {
//SubClass constructor code goes here
}
//Inherit the methods of BaseClass
SubClass.prototype = new BaseClass();
//Override the parent's getName method
SubClass.prototype.getName = function() {
return "SubClass";
}
//Alerts "SubClass"
alert(new SubClass().getName());
导致IE内存泄露
的Solution:
这种实现方式能够导致在IE中的内存泄漏,你应该尽量避免:
// 运行时的JavaScript Class 定义和继承
// 看上去很传统,但这些脚本会导致在Internet Explorer中的内存泄漏.
function BaseClass() {
this.getName = function() {
return "BaseClass";
};
//BaseClass constructor code goes here
}
function SubClass() {
//在对象实例建立时重载父类的getName方法
this.getName = function() {
return "SubClass";
}
//SubClass constructor code goes here
}
//Inherit the methods of BaseClass
SubClass.prototype = new BaseClass();
//Alerts "SubClass"
alert(new SubClass().getName());
就像我在第一个实现方法中所注释的那样,第一个实现方法有些丑陋,但它相比引起内存泄漏的第二种方式便是首选了。
我把这两种方法放在这里的目的是指出你不应该使用它们。
硬性编码的Solution:
让我们看一下第一个例子,它采用了标准的原型化方式,但问题是:它的子类方法如何调用父类(基类)方法?下面是一些开发者尝试并采用的方式:
一种企图进行父类调用的“通病”:
function BaseClass() { }
BaseClass.prototype.getName = function() {
return "BaseClass(" + this.getId() + ")";
}
BaseClass.prototype.getId = function() {
return 1;
}
function SubClass() {}
SubClass.prototype = new BaseClass();
SubClass.prototype.getName = function() {
//调用父类的getName()方法
//哈哈,这是对父类调用的直接引用吗?
return "SubClass(" + this.getId() + ") extends " +
BaseClass.prototype.getName();
}
SubClass.prototype.getId = function() {
return 2;
}
//输出结果:"SubClass(2) extends BaseClass(1)";
//这是正确的输出吗?
alert(new SubClass().getName());
上
面的代码是对第一段脚步进行修改后的版本,我去掉了一些注释和空格,使你能注意到新的getId()方法和对父类的调用。你一定急于知道通过这样对
BaseClass的硬性编码引用(hard coded reference),它是否能进行正确地调用BaseClass的方法?
一
个正确的、多态的父类调用必做的事情是保证“this”引用指向当前对象实例和类方法。在这里,看上去和它应该输出的结果非常接近,看上去好像在
SubClass中调用了BaseClass的getName()方法。你发现问题了吗?这个问题是非常细小的,但却很重要决不能忽视。通过使用上面的父
类调用语法,BaseClass的getName()方法被调用,它返回一个字符串:包括类名和“this.getId()”的返回值。问题在于
“this.getId()”应该返回2,而不是1。如果这和你所想的不同,你可以查看Java或者C#这类OO语言的多态性。
改进后的硬性编码Solution:
你可以通过一个微小的改动来解决这个问题。
静态(硬编码)父类调用:
function BaseClass() { }
BaseClass.prototype.getName = function() {
return "BaseClass(" + this.getId() + ")";
}
BaseClass.prototype.getId = function() {
return 1;
}
function SubClass() {}
SubClass.prototype = new BaseClass();
SubClass.prototype.getName = function() {
//一点魔法加上多态性!
//但很明显,这还是一个直接引用!
return "SubClass(" + this.getId() + ") extends " +
BaseClass.prototype.getName.call(this);
}
SubClass.prototype.getId = function() {
return 2;
}
//输出结果:"SubClass(2) extends BaseClass(2)";
//Hey, 我们得到了正确的输出!
alert(new SubClass().getName());
在ECMA-262 JavaScript/EcmaScript标准
中,Call()
方法是所有Function实例的一个成员方法,这已经被所有的主流浏览器所支持。JavaScript把所有的function看作对象,因此每个
function都具有方法和附着其上的属性。Call()方法允许你调用某个function,并在function的调用过程中确定“this”变量
应该是什么。JavaScript的function没有被紧紧地绑定到它所在的对象上,所以如果你没有显式地使用call()方法,“this”变量将
成为function所在的对象。
另外一种方法是使用apply方法,它和call()方法类似,只在参数上存在不同:apply()方法接受参数的数组,而call()方法接受单个参数。
Douglas Crockford
的Solution:
现在回溯到上面的示例,在这个示例中唯一的问题就是父类引用是直接的、硬性编写的。它可以适用于小型的类继承环境,但对于具有较深层次的大型继承来讲,这些直接引用非常难于维护。
那么,有解决方法吗?不幸的是这里没有简单的解决方案。
JavaScript没有提供对通过“隐性引用”方式调用父类方法的支持,这里也没有在其它OO语言中使用的“super”变量的等价物。于是,一些开发者做出了自己的解决方案,但就像我前面提到的那样,每个解决方案都存在某种缺点。
例如,下面列出的众多著名方法之一:JavaScript大师[ur=http://en.wikipedia.org/wiki/Douglas_Crockford]Douglas Crockford[/url]
在他的《Classical Inheritance in JavaScript》
中提出的方法。
Douglas Crockford的方法在多数情况下可以正常工作:
一次性支持代码:
//Crockford的方法:给所有的function都增加'inherits' 方法、
//每个类都增加了'uber'方法来调用父类方法
Function.prototype.inherits = function(parent) {
var d = 0, p = (this.prototype = new parent());
this.prototype.uber = function(name) {
var f, r, t = d, v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d -= 1;
return r;
};
};
运行示例:
function BaseClass() { }
BaseClass.prototype.getName = function() {
return "BaseClass(" + this.getId() + ")";
}
BaseClass.prototype.getId = function() {
return 1;
}
function SubClass() {}
SubClass.inherits(BaseClass);
SubClass.prototype.getName = function() {
//这里看上去非常的清晰,它调用了BaseClass的getName()方法
return "SubClass(" + this.getId() + ") extends " +
this.uber("getName");
}
SubClass.prototype.getId = function() {
return 2;
}
function TopClass() {}
TopClass.inherits(SubClass);
TopClass.prototype.getName = function() {
//这里看上去非常的清晰,它调用了SubClass的getName()方法
return "TopClass(" + this.getId() + ") extends " +
this.uber("getName");
}
TopClass.prototype.getId = function() {
//Ok, 因此this.getId()应该总是
//返回调用SubClass的getId()方法的返回值(2)。
return this.uber("getId");
}
//输出结果:"TopClass(2) extends SubClass(1) extends BaseClass(1)"
//嗯?后面的两次this.getId()调用都没有返回2.
//发生了什么?
alert(new TopClass().getName());
上
面代码的第一部分包括了Crockford的“inherit”和“uber”方法代码。第二部分看上去和前面的示例很类似,除了我添加了用来演示
Crockford方式所存在问题的第三层继承关系。诚然,Crockford这位JavaScript大师的方法是我所找到的最可靠的方法之一,我很敬
佩他在JavaScript编程方面做出的贡献。但是,如果你使用三个依次继承的类来考核他的代码,你将从输出中发现这里存在着细微的问题。
从
输出结果看,第一次调用的this.getId()返回了TopClass当前的id值“2”,但在调用SubClass和BaseClass的
getName()方法时返回了“1”而不是“2”。从代码上看,
在getName()方法中的父类调用行为是正确的,三个类的名字都被正确地显示出来。唯一的问题出现在this.uber("getId")这个父类调
用被放入调用堆栈(call
stack)时。因为此时当前对象是一个TopClass实例,而每次调用在调用堆栈中的this.getId()都应该返回调用TopClass的
getId()方法后的返回值。
而问题是TopClass的this.getId()方法通过
this.uber("getId")执行了父类调用,这三次this.getId()调用中的后两次错误地调用了BaseClass的getId()方
法,这样便在输出结果中显示了两次“1”。正确的行为应该是调用三次SubClass的getId()方法,在输出结果中显示三次“2”。大家可以通过FireFox的FireBug插件
进行代码debug进行观察。
这是十分难以描述的现象,我不能保证我能把它解释清楚。但是至少从上面的运行结果中可以看出它是错误的。
另外,Crockford的方法和其它一些方法的劣势在于每个父类调用都需要一个额外的方法调用和额外的某种处理。这是否成为你所面临的问题,取决于你所使用的父类调用深度。在ThinWire
项目的客户端代码中使用了大量的父类调用,因此父类调用的可靠性和快速性在项目中是很重要的。
我的初级Solution:
面对这样的窘境——Crockford的方法出现问题、在互联网上没有找到符合要求的方法,我决定看看我自己是否可以发明一种可以满足要求的方法。
这花掉了我近一周的时间来使代码工作并满足各种情况,但我对它的工作情况很有信心,并且很快把它与framework集成在一起,TinWire的beta和beta2两个版本中都使用了这些“初级设计”的代码。
动态父类调用:
一次性支持代码:
//定义最顶级类
function Class() { }
Class.prototype.construct = function() {};
Class.__asMethod__ = function(func, superClass) {
return function() {
var currentSuperClass = this.$;
this.$ = superClass;
var ret = func.apply(this, arguments);
this.$ = currentSuperClass;
return ret;
};
};
Class.extend = function(def) {
var classDef = function() {
if (arguments[0] !== Class) { this.construct.apply(this, arguments); }
};
var proto = new this(Class);
var superClass = this.prototype;
for (var n in def) {
var item = def[n];
if (item instanceof Function) {
item = Class.__asMethod__(item, superClass);
}
proto[n] = item;
}
proto.$ = superClass;
classDef.prototype = proto;
//赋给这个新的子类同样的静态extend方法
classDef.extend = this.extend;
return classDef;
};
运行示例:
//Hey, 注意一下这个类的定义方式
//看上去比其它方式要清楚些
var BaseClass = Class.extend({
construct: function() { /* optional constructor method */ },
getName: function() {
return "BaseClass(" + this.getId() + ")";
},
getId: function() {
return 1;
}
});
var SubClass = BaseClass.extend({
getName: function() {
//调用BaseClass的getName()方法
return "SubClass(" + this.getId() + ") extends " +
this.$.getName.call(this);
},
getId: function() {
return 2;
}
});
var TopClass = SubClass.extend({
getName: function() {
//调用SubClass的getName()方法
return "TopClass(" + this.getId() + ") extends " +
this.$.getName.call(this);
},
getId: function() {
//this.getId()总是返回调用父类的getId()方法的返回值(2)
return this.$.getId.call(this);
}
});
//输出结果:"TopClass(2) extends SubClass(2) extends BaseClass(2)"
//一切都正确!
alert(new TopClass().getName());
这
里是前面示例的,但是目前这种方式包括了通过“extend”方法实现的十分清晰的类定义模式和正确的父类调用语义。尤其是“extend”方法通过一个
中间function封装了类定义中的每个方法,这个中间function在每次方法调用时首先把当前父类引用“$”
与正确的父类引用相互交换,然后把这个正确的父类引用传递给apply()进行方法调用,最后再将把当前父类引用“$”
与正确的父类引用交换回来。这种方式唯一的问题就是它需要一些中间function,它们会对性能产生不良影响。所以近来我重新审视了设计、完成了去掉了
中间function了一种改良的方式。
改良后的Solution:
动态父类调用快速版本:
一次性支持代码
//定义最顶级类
function Class() { }
Class.prototype.construct = function() {};
Class.extend = function(def) {
var classDef = function() {
if (arguments[0] !== Class) { this.construct.apply(this, arguments); }
};
var proto = new this(Class);
var superClass = this.prototype;
for (var n in def) {
var item = def[n];
if (item instanceof Function) item.$ = superClass;
proto[n] = item;
}
classDef.prototype = proto;
//赋给这个新的子类同样的静态extend方法
classDef.extend = this.extend;
return classDef;
};
运行示例:
//Hey, 注意一下这个类的定义方式
//看上去比其它方式要清楚些
var BaseClass = Class.extend({
construct: function() { /* optional constructor method */ },
getName: function() {
return "BaseClass(" + this.getId() + ")";
},
getId: function() {
return 1;
}
});
var SubClass = BaseClass.extend({
getName: function() {
//调用BaseClass的getName()方法
return "SubClass(" + this.getId() + ") extends " +
arguments.callee.$.getName.call(this);
},
getId: function() {
return 2;
}
});
var TopClass = SubClass.extend({
getName: function() {
//调用SubClass的getName()方法
return "TopClass(" + this.getId() + ") extends " +
arguments.callee.$.getName.call(this);
},
getId: function() {
// this.getId()总是返回调用父类的getId()方法的返回值(2)
return arguments.callee.$.getId.call(this);
}
});
//输出结果:"TopClass(2) extends SubClass(2) extends BaseClass(2)"
//工作正常!而且没有任何中间function
alert(new TopClass().getName());
这是最后的设计,它使用了JavaScript中一点鲜为人知的特性:callee。
在
任何方法执行过程中,你可以查看那些通过“arguments”数组传入的参数,这是众所周知的,但很少有人知道“arguments”数组包含一个名为
“callee”的属性,它作为一个引用指向了当前正在被执行的function,而后通过“$”便可以方便的获得当前被执行function所在类的父
类。这是非常重要的,因为它是获得此引用的唯一途径(通过“this”对象获得的function引用总是指向被子类重载的function,而后者并非
全是正在被执行的function)。
原文作者附言:
Ok,这便是相对彻底的问题解决方案了。但是我想通过把它的细节写成文档让每个人阅读,以致我可以从中找出漏洞不断地完善代码。欢迎对我的文章进行评论和建议!
--Joshua Gertzen
参考资源:
ThinWire RIA Ajax GUI Framework
原文作者Joshua Gertzen的《Object Oriented Super Class Method Calling with JavaScript》
Douglas Crockford 的《Classical Inheritance in JavaScript》
Kevin Lindsey的《JavaScript Tutorial》
Tom Wright的《Super Simulation in JavaScript OOP》
Mozilla's Core JavaScript 1.5 手册
Jesse James Garrett的《Ajax: A New Approach to Web Applications》
Microsoft's HTML & DHTML 手册
W3C Document Object Model (DOM) 规范
ECMA-262 EcmaScript (JavaScript / JScript) 规范
感谢阅读此文
相关推荐
JavaScript密码强度检测JavaScript密码强度检测
本文将详细探讨如何使用JavaScript生成随机密码,以及提供的两个案例——js1.txt和js2.txt——中可能包含的具体实现方法。 生成随机密码的功能在许多应用场景中非常有用,比如用户注册时自动生成安全的初始密码,...
用JavaScript来实现密码强度颜色改变
通过使用JavaScript,我们可以创建一个用户友好的、自定义的密码生成器,允许用户根据需求选择密码的长度、字符类型等。 实现JavaScript随机密码生成器的关键在于理解JavaScript中的Math对象和字符串方法。Math对象...
基于JavaScript实现表单密码的隐蔽和显示出来 本文主要讲述了基于JavaScript实现表单密码的隐蔽和显示出来的方法。该方法通过使用HTML、CSS和JavaScript来实现密码的隐蔽和显示。通过给input元素添加type属性,并...
在这个“JavaScript密码保护和Session管理”的主题中,我们将深入探讨如何利用JavaScript实现用户登录的安全性和会话管理的有效性。 首先,密码保护是任何在线应用的基础。在`demo1.html`、`admin.html`等网页中,...
通过验证公众号密码来引的方法,打开网页会弹窗密码验证需要关注公众号回复关键词获取密码
zxcvbn 是 dropbox 开发的一个JavaScript密码强度估算库。 标签:zxcvbn
本项目是基于HTML5和JavaScript开发的密码学简介,包含120个文件,其中包括39个WOFF2字体文件、19个JPG图像文件、9个JavaScript脚本文件、9个PNG图像文件、8个HTML页面文件、8个CSS样式表文件、7个EOT字体文件、7个...
本文将深入探讨一个以“Python-一个易于定义规则的JavaScript密码生成器”为主题的项目,尽管标题提及的是Python,但描述中提到了JavaScript,这可能意味着项目包含了两种语言的应用。我们将主要关注JavaScript密码...
在这个"javascript实例应用---密码校验类.rar"压缩包中,我们可以预见到包含了一个或者多个JavaScript文件,它们可能实现了密码校验的功能,这对于任何需要用户输入密码的Web应用程序来说都是至关重要的。...
在本篇中,我们将深入探讨如何利用JavaScript实现密码强度检测,并提供一些相关策略和最佳实践。 首先,我们需要理解密码强度的评估标准。常见的衡量因素包括: 1. **长度**:通常,密码应至少包含8个字符,但更长...
dopass是一款开源的密码二次加密算法设计源码,采用JavaScript语言编写,集成了HTML和CSS,共包含1191个文件,涵盖697个PNG图片、259个JavaScript文件、78个HTML文件、77个CSS文件、52个JSON文件等多种类型。...
JavaScript是一种广泛应用于网页和网络应用开发的脚本语言,它主要负责处理客户端的交互和动态效果。在网页的注册表单中,密码验证是至关重要的一步,它确保用户输入的密码符合安全标准,如长度、字符类型等。本文将...
工作分享:因工作需要,纯手工写了一个JavaScript密码强度在线检测,分享出来,希望大家喜欢
标题中的“一个轻松添加密码强度计的javascript模块”指的是在网页应用中,使用JavaScript实现的一种功能,用于评估用户输入的密码的安全性。这种模块通常包括一套算法,用于判断密码的复杂度,例如长度、特殊字符、...
文章的贡献在于,研究人员基于现有的JavaScript密码算法库,集成并优化了国密算法。特别是对于SM2中的椭圆曲线固定点的标量乘运算,他们采用了固定基的comb方法进行优化,显著提升了密钥生成和签名的速度。通过这种...
在这个场景中,我们关注的是VS2015的安装过程中涉及到的证书问题以及两个特定的组件:JavaScript_ProjectSystem.msi和JavaScript_LanguageService.msi。 首先,关于“VS2015安装证书”,这通常是指安装过程中需要...
在这个示例中,我们可以看到一些JavaScript函数,例如`a1()、a2()、a3()、a4()、a5()`等函数,这些函数用于验证用户名、密码、Email等字段的输入值,并显示相应的错误信息或成功信息。 DOM操作 在这个示例中,我们...
JavaScript应用实例-密码加密.js