面向对象编程-Basic OOP / Class-Based Programming Concepts
JavaScript 是一种没有类(classless)、面向原型(prototype-oriented)的语言,它最强大的功能就是灵活。可以说基于类的编程(Class-based programming)是当前面向对象编程(Object Oriented Programming OOP)最流行的模式。这种方式更强调强类型(strong-typing)、封装(encapsulation)和标准编程规范(standard coding conventions)。
JavaScript 的灵活来自于不可预知(unpredictable)的成本。缺乏统一的结构,导致 JavaScript 的代码难于理解、维护和重用。另一方面,基于类的编程更像是可预知的(predictable)、可扩展的、可升级的。
幸运的是,Ext JS 的类系统结合了两种编程方式的特点,利用 JavaScript 的灵活性实现了基于类编程、灵活、可扩展、可升级的编程方式。
本指南将介绍以下几个方面的内容:
- 类和实例 - Classes and Instances
- 继承(多态)- Inheritance (polymorphism)
- 封装 - Encapsulation
类和实例-Classes and Instances
能够清晰地区分类和实例是非常重要的。简而言之,类是一个概念的蓝图,实例是这个蓝图的实现。来看一些示例:
- 建筑物是类,而宫殿是建筑物的实例;
- 狗是类,而Lassie(狗名)是狗的实例;
- 计算机是类,而你正在使用的电脑是计算机的实例;
类定义了它的实例的基础结构(structure)、属性(properties)和行为(behavior)。例如上述的类:
- 建筑物的所有实例都有几层的结构,地址和开放时间的属性。假设是智能建筑,它们具有关闭和上锁的行为。
- 狗的所有实例都有四条腿和一个尾巴的结构,还可以有狗名的属性。都有吠的行为。
- 计算机的所有实例都有 CPU 和存储器的结构,有型号的属性,具有打开和关闭的行为。
Let's define a class that will serve as our base for exploring concepts of class-based programming. We'll start with the "Square" class, which represents a square along with a simple method for calculating its area.
You can define the Square class with the following syntax:
让我们来定义一个类解释一下基于类编程的概念。我们将定义一个正方形的类,有一个边长的属性和一个计算面积的方法。
// Define a new class named: 'Square'
Ext.define('Square', {
// The side property represents the length of the side
// It has a default value of 0
side: 0,
// It also has a method to calculate its area
getArea: function() {
// We access the 'side' property to calculate area
return this.side * this.side;
}
});
// We use Ext.create to create an instance of our Square class
var sq = Ext.create('Square');
// The value of the 'side' property
// This is not the best way to do this, which we'll discuss below
sq.side = 4;
// Display a message and show the result of this calculation
Ext.Msg.alert('Message', 'The area is: ' + sq.getArea());
构造器-Constructors
让我们提供一个构造器来改进一下上述的类。构造器是一个特殊的函数,当类被实例化时将调用该函数。首先,我们改变一下设置边长的方式,通过构造器可以简化上述的示例:
Ext.define('Square', {
side: 0,
// This is a special function that gets called
// when the object is instantiated
constructor: function (side) {
// It receives the side as a parameter
// If defined, it is set as the square's side value
if (side) {
this.side = side;
}
},
getArea: function () {
return this.side * this.side;
}
});
// Thanks to the constructor, we can pass 'side's' value
// as an argument of Ext.create
// This is a slightly more elegant approach.
var sq = Ext.create('Square', 4);
// The passed value is assigned to the square's side property
// Display a message to make sure everything is working
Ext.Msg.alert('Message', 'The area is: ' + sq.getArea());
如果想传递更多的参数,可以使用一个对象传递:
Ext.define('Square', {
side: 0,
// We have added two more configs
color: 'red',
border: true,
// Pass a config object, which contains 'side's' value
constructor: function(config) {
// Once again, this is not yet the best syntax
// We'll get to that in the next example
if (config.side) {
this.side = config.side;
}
if (config.color) {
this.color = config.color;
}
// border is a boolean so we can skip the if block
this.border = config.border;
},
getArea: function() {
return this.side * this.side;
}
});
// We pass an object containing properties/values
var sq = Ext.create('Square', {
side: 4,
border: false
});
// Now display a message that uses the other two properties
// Note that we're accessing them directly (i.e.: sq.color)
// This will change in the next section
Ext.Msg.alert('Message',
['The area of the',sq.color,'square',
(sq.border?'with a border':''),'is:',
sq.getArea()].join(' ')
);
应用-Apply
使用 Ext.apply
可以进一步简化构造器。 Ext.apply
将 config
的属性复制到指定的对象。
Ext.define('Square', {
side: 0,
color: 'red',
border: true,
constructor: function(config) {
// Use Ext.apply to not set each property manually
// We'll change this again in the "Inheritance" section
Ext.apply(this,config);
},
getArea: function() {
return this.side * this.side;
}
});
var sq = Ext.create('Square', {
side: 4,
border: false
});
Ext.Msg.alert('Message',
['The area of the',sq.color,'square',
(sq.border?'with a border':''),'is:',
sq.getArea()].join(' ')
);
定义更多的类-Defining more classes
我们再定义一些类: Circle
和 Rectangle
,看看与 Square
有哪些不同。
Ext.define('Square', {
side: 0,
color: 'red',
border: true,
constructor: function(config) {
Ext.apply(this, config);
},
getArea: function() {
return this.side * this.side;
}
});
Ext.define('Rectangle', {
//Instead of side, a rectangle cares about base and height
base: 0,
height: 0,
color: 'green',
border: true,
constructor: function(config) {
Ext.apply(this, config);
},
getArea: function() {
// The formula is different
return this.base * this.height;
}
});
Ext.define('Circle', {
// A circle has no sides, but radius
radius: 0,
color: 'blue',
border: true,
constructor: function(config) {
Ext.apply(this, config);
},
getArea: function() {
// Just for this example, fix the precision of PI to 2
return Math.PI.toFixed(2) * Math.pow(this.radius, 2);
}
});
var square = Ext.create('Square', {
side: 4,
border: false
}),
rectangle = Ext.create('Rectangle', {
base: 4,
height: 3
}),
circle = Ext.create('Circle', {
radius: 3
});
// This message will now show a line for each object
Ext.Msg.alert('Message', [
['The area of the', square.color, 'square',
(square.border ? 'with a border' : ''), 'is:',
square.getArea()].join(' '),
['The area of the', rectangle.color, 'rectangle',
(rectangle.border ? 'with a border' : ''), 'is:',
rectangle.getArea()].join(' '),
['The area of the', circle.color, 'circle',
(circle.border ? 'with a border' : ''), 'is:',
circle.getArea()].join(' ')
].join('<br />'));
继承-Inheritance
讲解继承之前,先回顾一下上述的示例。下面的示例中,我们为 Square
添加了一个方法 getShapeName
,再改变测试信息的生成方式:
Ext.define('Square', {
side: 0,
color: 'red',
border: true,
constructor: function(config) {
Ext.apply(this, config);
},
getArea: function() {
return this.side * this.side;
},
// This function will return the name of this shape
getShapeName: function () {
return 'square';
}
});
//This function generates a sentence to display in the test dialog
function generateTestSentence(shape) {
return ['The area of the', shape.color, shape.getShapeName(),
(shape.border ? 'with a border' : ''),
'is:', shape.getArea()].join(' ');
}
var square = Ext.create('Square', {
side: 4,
border: false
});
Ext.Msg.alert('Message', generateTestSentence(square));
我们再为 Rectangle
和 Circle
添加同样的方法。你会注意到有很多的重复代码,这将导致代码很难维护并且容易出错。继承(inheritance)就是为了减少重复代码,使代码更容易理解和维护。
父类和子类-Parent and child classes
为了应用继承的概念,减少重复代码,我们把子类的属性放到父类中:
// The shape class contains common code to each shape class
// This allows the passing of properties on child classes
Ext.define('Shape', {
// Let's define common properties here and set default values
color: 'gray',
border: true,
// Let's add a shapeName property and a method to return it
// This replaces unique getShapeName methods on each class
shapeName: 'shape',
constructor: function (config) {
Ext.apply(this, config);
},
getShapeName: function () {
return this.shapeName;
}
});
Ext.define('Square', {
// Square extends from Shape so it gains properties
// defined on itself and its parent class
extend: 'Shape',
// These properties will 'override' parent class properties
side: 0,
color: 'red',
shapeName: 'square',
getArea: function() {
return this.side * this.side;
}
});
//This function generates a sentence to display in the test dialog
function generateTestSentence(shape) {
return ['The area of the', shape.color, shape.getShapeName(),
(shape.border ? 'with a border' : ''),
'is:', shape.getArea()].join(' ');
}
var square = Ext.create('Square', {
side: 4
});
// Since Square extends from Shape, this example will work since
// all other properties are still defined, but now by 'Shape'
Ext.Msg.alert('Message',
[ generateTestSentence(square) ].join('<br />'));
我们甚至可以将 generateTestSentence()
方法放到 Shape
类中:
Ext.define('Shape', {
color: 'gray',
border: true,
shapeName: 'shape',
constructor: function (config) {
Ext.apply(this, config);
},
getShapeName: function () {
return this.shapeName;
},
// This function will generate the test sentence for this shape,
// so no need to pass it as an argument
getTestSentence: function () {
return ['The area of the', this.color, this.getShapeName(),
(this.border ? 'with a border' : ''),
'is:', this.getArea()].join(' ');
}
});
Ext.define('Square', {
extend: 'Shape',
side: 0,
color: 'red',
shapeName: 'square',
getArea: function() {
return this.side * this.side;
}
});
var square = Ext.create('Square', {
side: 4
});
// The generateTestSentence function doesn't exist anymore
// so use the one that comes with the shape
Ext.Msg.alert('Message',
[ square.getTestSentence() ].join('<br />'));
封装-Encapsulation
在上述示例中,你可能注意到,我们通过实例直接访问它的属性,例如:使用 square.color
可以获取颜色,也可以改变颜色。
To prevent direct read/write of an object's properties, we’ll make use of Ext JS’ config block. This will automatically restrict access to the object's properties so they can only be set and retrieved using accessor methods.
Accessor methods are automatically generated getters and setters for anything in a class's config block. For instance, if you have shapeName in a config block, you get setShapeName() and getShapeName() by default.
Note: The config block should only include new configs unique to its class. You should not include configs already defined in a parent class's config block.
为了禁止直接读写对象的属性,我们需要使用 Ext JS 的配置块(config block
)功能。这将自动限制对象属性的访问,只能通过访问器(accessor
)方法进行访问。
访问器方法是通过配置块自动生成的getters
和 setters
方法。例如:配置块中定义了shapeName
,你将缺省得到setShapeName()
和 getShapeName()
两个方法。
注意:配置块中应该只包括当前类新增的配置,不应该包括已经在父类中定义的配置。
Ext.define('Shape', {
// All properties inside the config block have
// their accessor methods automatically generated
config: {
color: 'gray', // creates getColor|setColor
border: true, // creates getBorder|setBorder
shapeName: 'shape' // creates getShapeName|setShapeName
},
constructor: function (config) {
Ext.apply(this, config);
// Initialize the config block for this class
// This auto-generates the accessor methods
// More information on this in the next section
this.initConfig(config);
},
// We have removed the getShapeName method
// It's auto-generated since shapeName is in the config block
// Now we can use the accessor methods instead
// of accessing the properties directly
getTestSentence: function () {
return ['The area of the', this.getColor(),
this.getShapeName(),
(this.getBorder() ? 'with a border' : ''), 'is:',
this.getArea()].join(' ');
}
});
Ext.define('Square', {
extend: 'Shape',
// In a child class, the config block should only
// contain new configs particular for this class
config: {
side: 0 // getSide and setSide are now available
},
// Parent class properties are defined outside the config block
color: 'red',
shapeName: 'square',
getArea: function() {
// We're using the accessor methods of the 'side' config
return this.getSide() * this.getSide();
}
});
var square = Ext.create('Square', {
side: 4
});
// The following line won't modify the value of 'side' anymore
square.side = 'five';
// To modify it instead, we'll use the setSide method:
square.setSide(5);
// The area will be reported as 25
Ext.Msg.alert('Message',
[ square.getTestSentence() ].join('<br />'));
Ext.Base类-Ext.Base class
在 Ext JS 中,所有类都是一个共通基类的子类,除非显示指定。这个基类就是 Ext.base
。
就像 Square
继承自 Shape
一样, Shape
自动继承自 Ext.base
。
Ext.define('Shape', {
// Properties and methods here
});
实际上等同于:
Ext.define('Shape', {
extend: 'Ext.Base'
// Properties and methods here
});
这也是为什么我们可以在 Shape
的构造器中使用 this.initConfig(config)
的原因。 initConfig()
是 Ext.base
的方法,所有继承它的子类都继承该方法。 initConfig()
方法为它的类初始化配置块,自动生成访问器方法。
真实的属性封装-Real property encapsulation
封装属性的目的是为了保护对象不被随意和非法的修改。这些修改难免会导致错误。
例如:当使用配置块避免直接修改属性时,目前还没有限制不合法数据的存取。也就是说没有阻止调用square.setSide('five')
的方式,而且这种调用还将导致错误。
可以通过 apply
方法阻止这种情况。 Apply 是模板方法,允许在实际修改前检验传递的数据。该方法将config的所有属性复制到一个指定的对象。
由于 side
是配置块的一个属性,所以我们可以利用这个模板方法在实际修改前进行一些处理,例如检查新值是否是一个数字。
Ext.define('Shape', {
config: {
color: 'gray',
border: true,
shapeName: 'shape'
},
constructor: function (config) {
Ext.apply(this, config);
this.initConfig(config);
},
getTestSentence: function () {
return ['The area of the', this.getColor(),
this.getShapeName(),
(this.getBorder() ? 'with a border' : ''),
'is:', this.getArea()].join(' ');
}
});
Ext.define('Square', {
extend: 'Shape',
config: {
side: 0
},
color: 'red',
shapeName: 'square',
getArea: function() {
return this.getSide() * this.getSide();
},
// 'side' is a property defined through the 'config' block,
// We can use this method before the value is modified
// For instance, checking that 'side' is a number
applySide: function (newValue, oldValue) {
return (Ext.isNumber(newValue)? newValue : oldValue);
}
});
var square = Ext.create('Square', {
side: 4
});
// The following line won't modify the value of 'side'
square.setSide('five');
// The area will be reported as 16
Ext.Msg.alert('Message',
[ square.getTestSentence() ].join('<br />'));