面向对象的 JavaScript 菜单和其他控件





4.00/5 (3投票s)
使用面向对象编程原理编写 JavaScript 控件。
引言
本文是对 3 月 31 日(已被删除)文章的修改版本,附带的库同时兼容 Internet Explorer 和 Firefox 浏览器。文章介绍了 JavaScript 如何通过一些简单的约定、规则和技巧来弥补其不支持面向对象特性的不足。此外,我们还提供了下载一些基本的 JavaScript 控件,包括 Menu
、TextBox
、Checkbox
、TableLayout
,它们都是使用这些方法编写的。要查看示例并获取更高级的控件,如 Grid
、ComboBox
、Calendar
、各种输入掩码,请访问我们的 网站。
如果您熟悉标准功能,请跳过以下部分。
标准面向对象 JavaScript 特性
自定义 JavaScript 对象(为简单起见,我们称之为类)可以定义为一个函数
function Component(name, data)
{
this._name = name;// public property
this._data = data;// public property
var someStaticData = null;// private static variable
this.prototype.setName = _setName;// public member function
this.prototype.getName = _getName;// public member function
this.prototype.setData = _setData;// public member function
this.prototype.getData = _getData;// public member function
// function implementation
function _setName(name)
{
this._name = name;
}
function _getName()
{
return this._name;
}
function _setData(data)
{
this._data = data;
}
function _getData()
{
return this._data;
}
function someStaticFunction()
{
//some code here
}
}
从上面的示例可以看出,在 Component
类上定义了四个 public
方法。可以像这样创建类的实例并调用实例上的方法:
var component = new Component();
component.setName("SomeName");
component.setData("some string data");
此外,_name
和 _data
字段是类的属性。不幸的是,JavaScript 类中的所有属性都可以从外部访问,它们都是 public
的。
component._name = "SomeName";
在类内部声明的变量(参见 someStaticData
)则不同。它们不能从外部访问,它们是 private
的。那么它们有什么用呢?它们是 static
的,并且这些变量在类的所有实例之间共享。
如果方法未分配给类的原型(参见上面的 someStaticFunction
),它就像上面描述的 private static
变量一样——它不应该在其实现中包含任何类方法或属性。
扩展的面向对象约定
本章将解释如何利用 JavaScript 的有限能力使其成为一种面向对象的语言。我们还将展示一些优化性能的编程技巧。
初始化器
看看之前的 Component
类,我们如何使其可重用或实现第一个 OO 特性——继承?如果我们定义一个名为 Initializer
的函数,它接受一个参数(类),并为其分配所有必需的方法(为方便起见,我们使用后缀 T),会怎么样?
function ComponentT(obj)
{
obj.prototype.setName = _setName;// public member function
obj.prototype.getName = _getName;// public member function
obj.prototype.setData = _setData;// public member function
obj.prototype.getData = _getData;// public member function
// function implementation
function _setName(name)
{
this._name = name;
}
function _getName()
{
return this._name;
}
function _setData(data)
{
this._data = data;
}
function _getData()
{
return this._data;
}
}
请注意,在原型赋值中,我们使用参数 obj
代替此操作。
然后我们从 Component
类中删除所有方法声明和实现:
function Component(name, data)
{
this._name = name;// public property
this._data = data;// public property
}
使用 Initializer
还能带来巨大的性能提升——尝试创建 100 个类实例(在类构造函数中放入 10 个方法),然后对同一个类使用 Initializer
,第二种方法的工作速度将是原来的 10 倍,因为原型只分配一次,而不是在每次创建对象时都进行。在这里,我们来实现第一个 OOP 特性——继承。
继承
现在,我们添加这一行代码:
ComponentT(Component);
发生了什么?当调用函数时,它将 ComponentT
函数中的所有方法分配给 Component
—— Component
继承了 ComponentT
。
如果只有一个 Component
,这没什么用,但如果我们想在 Control
类中扩展它的功能怎么办?
function Control(parent)
{
this._parent = parent;// public property
}
然后我们可以这样调用它:
ComponentT(Control);
Control
类现在继承自 Component
。如果您想继承自某个特定类,应该提供 Initializer
。因此,我们为 Control
类做同样的事情,但现在我们删除之前的代码行,将其放入 Initializer
中:
ControlT(Control);
function ControlT(obj)
{
ComponentT(obj);
obj.prototype.setParent = _setParent;// public member function
obj.prototype.getParent = _getParent;// public member function
obj.prototype.paint = _paint;// public member function
function _setParent(parent)
{
this._parent = parent;
}
function _getParent()
{
return this._parent;
}
function paint()
{
// some code
}
}
如果我们创建 TextBox
,它扩展了 Control
,而 Control
扩展了 Component
,那么图景会更清晰(如果您还不明白的话):
function TextBox(parent, text)
{
this._text = text;
}
TextBoxT(TextBox);
function TextBoxT(obj)
{
ControlT(obj);
obj.prototype.setText = _setText;// public member function
obj.prototype.getText = _getText;// public member function
obj.prototype.paint = _paint;// public member function
function _setText(text)
{
this._text = text;
}
function _getText()
{
return this._text;
}
function paint()
{
// some code
}
}
这还没完,那些未继承的属性怎么办?
这个问题可以通过一个简单的约定来解决:
- 我们从所有类函数(
Component
、Control
、Textbox
)内部删除所有属性。 - 在每个类的
Initializer
中放置一个成员函数,并将其命名为initialize
。 - 在该函数的实现中,放置相应的属性。
- 从每个构造函数中调用此函数。
完整的最终代码将是:
function Component(name, data)
{
this.initialize(name, data);
}
function Control(parent)
{
this.initialize(parent);
}
function TextBox(parent, text)
{
this.initialize(parent, text);
}
function ComponentT(obj)
{
obj.prototype.initialze = _initialze;// public member function
obj.prototype.setName = _setName;// public member function
obj.prototype.getName = _getName;// public member function
obj.prototype.setData = _setData;// public member function
obj.prototype.getData = _getData;// public member function
// function implementation
function _initialze(name)
{
this.setName(name);
}// rest of implementation here
}
function ControlT(obj)
{
ComponentT(obj);
obj.prototype.initialze = _initialze;// public member function
obj.prototype.setParent = _setParent;// public member function
obj.prototype.getParent = _getParent;// public member function
obj.prototype.paint = _paint;// public member function
function _initialze(parent)
{
Component.prototype.initialze.call(this);
this.setParent(parent);
this.paint();
}
// rest of implementation here
}
function TextBoxT(obj)
{
ControlT(obj);
obj.prototype.initialze = _initialze;// public member function
obj.prototype.setText = _setText;// public member function
obj.prototype.getText = _getText;// public member function
obj.prototype.paint = _paint;// public member function
function _initialze(parent, text)
{
Control.prototype.initialze.call(this, parent);
this.setText(text);
}// rest of implementation here
}
现在一切都已继承。请注意 TextBoxT
的 initialize
方法中这个巧妙的 Control.prototype.initialze.call(this, parent);
。如果您不知道,它会调用 Control
的 initialize
方法实现,并将自身作为引用传递,告知函数内部的 this
实际上将传递 TextBox
实例。它还将 parent
作为第一个参数传递。这允许调用基类方法。正如您所见,我们遇到了下一个 OOP 特性——多态。
多态
从上面的示例中,请注意层次结构中的每个类都有自己的 initialize
方法,该方法被子类覆盖。那么,如果您有一个 TextBox
实例,它扩展了 Control
,并通过调用 initialize
方法创建,而 initialize
又调用 Control
的 initialize
方法,而 Control
的 initialize
方法又调用 paint
方法,会发生什么?
var txtName = new TextBox(parent, "John Smith");
两个类都有 paint
方法,但将调用 TextBox
的正确方法——这就是所谓的多态。
静态方法
您可以通过以下方式声明 static
方法:
- 它被声明为普通的成员函数。
- 在其实现中,您不能使用类的非
static
属性或函数。 - 它通过类构造函数从外部调用:
Control.prototype.foo();
接口
接口被创建为类,区别在于它们没有属性,并且在其函数实现中,会抛出一个错误——这迫使开发人员在接口是类层次结构的一部分时实现该方法。
function BinderT(obj)
{
IBinder(obj);
obj.prototype.setControl = _setControl;// public member function
obj.prototype.getControl = _getControl;// public member function
}
function IBinder(obj)
{
obj.prototype.setControl = _setControl;// public member function
obj.prototype.getControl = _getControl;// public member function
function _setControl()
{
throw "Please implement setControl method.";
}
function _getControl()
{
throw "Please implement getControl method.";
}
}
历史
- 2008年4月7日:初始帖子