编写面向对象的 JavaScript 第一部分






4.85/5 (125投票s)
2003年12月9日
7分钟阅读

584840

3271
使用 Cfx 开发 JavaScript 控件类库。
引言
ASP.NET和Visual Studio 7.0正在为改善Web开发体验做出重要贡献。然而,开发者倾向于限制与JavaScript的交互。显然,JavaScript对于为网页添加客户端功能非常有价值。但是,ASP.NET编程模型建议开发者在生成客户端JavaScript的同时生成页面布局,而ASP.NET控件会发出这些JavaScript。因此,这种模型倾向于将JavaScript限制为过程式附属。这相当不幸,因为它严重限制了开发者可以用来编写丰富且可重用的客户端组件的“面向对象”脚本语言的强大功能。
JavaScript面向对象功能未被充分利用有几个原因:
- 客户端操作倾向于离散化,偏向于过程。
- ASP.NET的控件编程模型建议将JavaScript限制为功能性附属。
- 旧的JavaScript缺乏关键功能,如异常处理和内部函数。
- JavaScript教科书经常不强调其对面向对象编程的支持。
- JavaScript不像传统的OOP语言那样正式支持类继承。
但是,以过程式方式编写JavaScript会产生另一系列不良状况:
- 由于数据和函数的解耦导致任务重复。
- 倾向于使用复杂的数组模式来模拟抽象数据类型。
- 增加了代码修改的影响风险和副作用。
- 使函数审计复杂化,从而降低了可重用性。
- 由此产生的效果是性能下降。
我们可以通过以面向对象的方式编写JavaScript来增强我们的脚本并消除这些问题。JavaScript拥有所有熟悉的面向对象编程的原则,通过其定义新对象类型并通过其“原型”属性扩展它们的能力。原型充当一整类抽象数据类型的模板。因此,原型属性被该类的所有实例化对象共享。虽然这是一种强大且富有表现力的继承模型,但缺失的是传统面向对象语言的程序员熟悉的习惯性类继承支持。
面向对象的JavaScript编写将分三篇文章介绍。第一部分提供有关JavaScript如何支持面向对象编程主要原则的背景。第二部分演示如何使用JavaScript构造来构建类继承框架,并编写支持JavaScript类层次结构的脚本。第三部分也是最后一部分将展示如何使用JavaScript类框架来构建ASP.NET用户控件的面向对象的客户端抽象。
本系列文章旨在介绍一种替代的编程策略和风格,该策略和风格充分利用JavaScript的功能,然后将JavaScript从其典型的初级角色(由ASP.NET编程模型强化)提升为与ASP.NET完全的域伙伴关系。其结果将使开发人员能够编写丰富的客户端脚本。
JavaScript中的面向对象编程原则
抽象数据类型
抽象数据类型是现实世界构造的描述和表示。许多语言支持聚合不同的内在数据类型来表示某个现实世界的构造。例如,在“C”中,`struct`关键字表示这种聚合。然而,抽象数据类型不仅仅是不同数据类型的聚合。抽象数据类型还定义了由抽象表示的行为。在许多面向对象语言中,数据及其相关行为的组合构成了“类”。C++、Java和C#等语言提供了`class`关键字来标识抽象数据类型。
虽然JavaScript保留了`class`关键字,但它不支持像传统OOP语言那样定义类。在JavaScript中,函数被用作对象描述符,所有函数实际上都是JavaScript对象。因此,在JavaScript中,类是函数定义。下面的代码演示了在JavaScript中定义最简单的类——空类`MyClass`。
function MyClass() {} |
图 1. 空类定义 |
使用JavaScript的`new`运算符将`myClassObj`定义为`MyClass`类型的对象实例。
var myClassObj = new MyClass(); |
图 2. 对象实例化 |
请注意,`function`关键字是重载的,既可以作为对象的构造函数,也可以作为过程式函数。
var result = MyClass(); |
图 3. MyClass用作过程式函数 |
`MyClass`被解释为构造函数还是过程,区别在于`new`运算符。`new`运算符实例化一个`MyClass`类的对象,调用构造函数;而在第二次调用中,是对`MyClass`函数进行过程式调用,并期望一个返回值。
图 1 中定义的`MyClass`是一个空类,没有任何指定的数据或行为。JavaScript可以动态地向`MyClass`的实例化对象添加属性。
var myClassObj = new MyClass();
myClassObj.myData = 5;
myClassObj.myString = "Hello World";
alert( myClassObj.myData ); // displays: 5
alert( myClassObj.myString ); // displays: "Hello World"
|
图 4. 动态分配对象属性 |
然而,问题在于“只有”由`myClassObj`引用的`MyClass`实例拥有附加的数据属性。后续实例将不具有任何属性。我们需要一种方法来为`MyClass`的所有实例定义属性。通过在构造函数中使用`this`关键字,数据属性现在被定义在`MyClass`的所有实例上。
function MyClass()
{
this.myData = 5;
this.myString = "Hello World";
}
var myClassObj1 = new MyClass();
var myClassObj2 = new MyClass();
myClassObj1.myData = 10;
myClassObj1.myString = "Obj1: Hello World";
myClassObj2.myData = 20;
myClassObj2.myString = "Obj2: Hello World";
alert( myClassObj1.myData ); // displays: 10
alert( myClassObj1.myString ); // displays: "Obj1: Hello World"
alert( myClassObj2.myData ); // displays: 20
alert( myClassObj2.myString ); // displays: "Obj2: Hello World"
|
图 5. 定义类的所有实例的数据属性 |
`MyClass`仍然不完整,因为它没有指定任何行为。要向`MyClass`添加方法,需要向`MyClass`添加引用函数的属性。
function MyClass()
{
this.myData = 5;
this.myString = "Hello World";
this.ShowData = DisplayData;
this.ShowString = DisplayString;
}
function DisplayData()
{
alert( this.myData );
}
function DisplayString()
{
alert( this.myString );
}
var myClassObj1 = new MyClass();
var myClassObj2 = new MyClass();
myClassObj1.myData = 10;
myClassObj1.myString = "Obj1: Hello World";
myClassObj2.myData = 20;
myClassObj2.myString = "Obj2: Hello World";
myClassObj1.ShowData(); // displays: 10
myClassObj1.ShowString(); // displays: "Obj1: Hello World"
myClassObj2.ShowData(); // displays: 20
myClassObj2.ShowString(); // displays: "Obj2: Hello World"
|
图 6. 定义类的所有实例的数据和方法 |
上图在JavaScript中定义了一个完整的抽象数据类型或类。`MyClass`现在定义了一个具有数据和相关行为的具体类型。
封装
使用上文中定义的`MyClass`允许访问其内部数据表示,并且其方法和变量名是全局作用域的,增加了名称冲突的风险。封装支持数据隐藏,并将对象视为提供服务的自包含实体。
function MyClass()
{
var m_data = 5;
var m_text = "Hello World";
this.SetData = SetData;
this.SetText = SetText;
this.ShowData = DisplayData;
this.ShowText = DisplayText;
function DisplayData()
{
alert( m_data );
}
function DisplayText()
{
alert( m_text );
return;
}
function SetData( myVal )
{
m_data = myVal;
}
function SetText( myText )
{
m_text = myText;
}
}
var myClassObj1 = new MyClass();
var myClassObj2 = new MyClass();
myClassObj1.SetData( 10 );
myClassObj1.SetText( "Obj1: Hello World" );
myClassObj2.SetData( 20 );
myClassObj2.SetText( "Obj2: Hello World" );
myClassObj1.ShowData(); // displays: 10
myClassObj1.ShowText(); // displays: "Obj1: Hello World"
myClassObj2.ShowData(); // displays: 20
myClassObj2.ShowText(); // displays: "Obj2: Hello World"
|
图 7. 动态为所有对象实例分配属性 |
JavaScript将类定义视为函数定义,并使用`var`关键字定义局部变量。因此,`var`表明`m_data`是`MyClass`的局部或“私有”变量。在图 6 中,未使用`var`,因此`MyData`是全局作用域的或“公共”的。`var`关键字是JavaScript中指定封装的方式。
继承
JavaScript通过使用对象原型来支持继承。原型是所有对象类型实例共享的属性模板。因此,对象类型的实例“继承”其原型属性的值。在JavaScript中,所有对象类型都有一个可以扩展和继承的原型属性。
在下面的示例中,`Shape`原型对象定义了三个属性:`GetArea`、`GetPerimeter`和`Draw`,它们引用函数`Shape_GetArea`、`Shape_GetParameter`和`Shape_Draw`。每个`Shape`实例都继承了原型,允许它们通过属性调用`Shape`函数。
为了在JavaScript中实现继承,将先前定义的某个对象类型的原型复制到派生对象类型的原型中。下面的`Shape`原型被复制到派生`Circle`和`Rectangle`对象类型的原型中。然后,`Draw`属性被覆盖,为新类提供正确的函数引用。
Shape.prototype.GetArea = Shape_GetArea;
Shape.prototype.GetParameter = Shape_GetParameter;
Shape.prototype.Draw = Shape_Draw;
function Shape()
{
}
function Shape_GetArea()
{
return this.area;
}
function Shape_GetParameter()
{
return this.parameter;
}
function Shape_Draw()
{
alert( "Drawing generic shape" );
}
Circle.prototype = new Shape();
Circle.prototype.constructor = Circle;
Circle.prototype.baseClass = Shape.prototype.constructor;
Circle.prototype.Draw = Circle_Draw;
function Circle( r )
{
this.area = Math.PI * r * r;
this.parameter = 2 * Math.PI * r;
}
function Circle_Draw()
{
alert( "Drawing circle" );
}
Rectangle.prototype = new Shape();
Rectangle.prototype.constructor = Rectangle;
function Rectangle( x, y )
{
this.area = x * y;
this.parameter = 2 * x + 2 * y;
}
Rectangle.prototype = new Shape();
Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.baseClass = Shape.prototype.constructor;
Rectangle.prototype.Draw = Rectangle_Draw;
function Rectangle( x, y )
{
this.area = x * y;
this.parameter = 2 * x + 2 * y;
}
function Rectangle_Draw()
{
alert( "Drawing rectangle" );
}
var circle = new Circle( 10 );
var rectangle = new Rectangle( 10, 20 );
alert( "Circle base class = " + circle.baseClass );
alert( "Circle area = " + circle.GetArea() );
alert( "Circle parameter = " + circle.GetParameter() );
circle.Draw();
alert( "Rectangle base class = " + rectangle.baseClass );
alert( "Rectangle area = " + rectangle.GetArea() );
alert( " Rectangle parameter = " + rectangle.GetParameter() );
rectangle.Draw();
|
图 8. JavaScript继承 |
`Circle`和`Rectangle`原型通过对象实例被分配到`Shape`类的原型,从而“继承”了分配给`Shape`类的原型数组的方法。需要`Shape`类的实例才能创建`Shape`原型数组的副本。这允许覆盖`Shape`方法,同时保留与所有`Shape`对象相关联的原始原型数组。
`Circle`和`Shape`类都通过覆盖`Draw`方法来扩展`Shape`类,提供各自的实现,同时继承`Shape`类`GetArea`和`GetParameter`方法的实现。
var circle = new Circle( 10 );
var rectandle = new Rectangle( 10, 20 );
alert( "Circle base class = " + circle.baseClass );
// Alert: Circle base class = <STRING constructor Shape of output>
alert( "Circle area = " + circle.GetArea() );
// Alert: Circle area = 314.1592653589793
alert( "Circle parameter = " + circle.GetParameter() );
// Alert: Circle parameter = 62.83185307179586
circle.Draw();
// Alert: Drawing circle
alert( "Rectangle base class = " + rectangle.baseClass );
// Alert: Rectangle base class = <STRING constructor Shape of output>
alert( "Rectangle area = " + rectangle.GetArea() );
// Alert: Rectangle area = 200
alert( "Rectangle parameter = " + rectangle.GetParameter() );
// Alert: Rectangle parameter = 60
rectangle.Draw();
// Alert: Drawing rectangle
|
图 9. JavaScript继承的结果 |
多态
多态性定义了对象对同一函数调用的不同行为和动作。`Shape`、`Circle`和`Rectangle`对象类型的`Draw`属性是多态的。`Draw`属性根据其对象类型调用不同的函数。
var shape = new Shape();
var circle = new Circle( 10 );
var rectandle = new Rectangle( 10, 20 );
shape.Draw();
// Alert: Drawing generic shape
circle.Draw();
// Alert: Drawing circle
rectangle.Draw();
// Alert: Drawing rectangle
|
图 10. JavaScript多态性 |
演示
演示程序`JsOOPDemo`是一个标准的ASP.NET应用程序,仅运行JavaScript,以一个空白网页作为其UI。该网站的URL是*https:///JsOOP/JsOOPDemo/WebForm1.aspx*。这意味着您应该将源代码复制到您的*https://*主目录下的*JsOOP/JsOOPDemo*子目录中。您可以使用Visual Studio创建ASP.NET项目,或使用“控制面板”中的“管理工具”中的IIS管理工具。此演示已在运行时版本1.0和1.1上进行了测试。
演示程序包含文章中JavaScript的示例,运行演示程序将帮助您探索使用JavaScript编写面向对象代码。
结论
JavaScript原型和函数对象提供了面向对象语言特有的面向对象特性。令人困惑的是,JavaScript本身并不直接支持类继承,而是通过原型继承笨拙地支持它。本系列文章的第二部分将介绍一个简化JavaScript类继承的框架,并演示编写JavaScript对象类型的类层次结构。
历史
- 版本 1.0