基础 OOP / Class-Based 编程概念

JavaScript是一个无类的, 面向原型语言 并且它最强大的特性是灵活性. 这就是说, 基于类的编程是有争议的最流行的面向对象编程的模型(OOP). 这种方式通常强调强类型,封装和标准编码规范.

JavaScript 的灵活性伴随而来的是不稳定性的代码.没有一个统一的结构 JavaScript 代码难于理解,维护和重用.在另一方面,基于类的编程是更稳定,易于扩展和随着时间的推移可升级的.

幸运地是, Ext JS’ 类体系提供给了你最好的二个世界.你获得了灵活,可扩展的并且易于升级的用 JavaScript的灵活性的基于类的编编程的一种实现.

本手册面对那些有意学习和复习 Ext JS面向对象和基于类的编程的任意开发人员.我们将包含如下主题:

  • 类和实例

  • 继承 (多态)

  • 封装

类和实例

能清晰的区分类和实例很重要.简单来说,一个类是一个概念的蓝图,而实例是这个蓝图的实现.我们看一些例子:

  • “Building(建筑)” 是一个类, 而帝国大厦是“Building(建筑)”的一个实例.

  • “Dog(狗)” 是一个类, 而莱莤是“Dog(狗)”的实例.

  • “Computer(计算机)” 是一个类, 而你正在使用的计算机是“Computer(计算机)” 的一个实例.

image alt text

一个类定义了基础结构,属性和它实例的行为.例如使用上面描述的相同的类:

  • 所有 “Building(建筑物)” 的实例都有给定数的楼层 (结构), 一个地址, 和开放时间 (属性). 同时, 假定是 “智能建筑”, 它们能根据需要关闭和锁好大门 (行为).

  • 所有 “Dog(狗)” 的实例都有 4 条腿和 1 条尾巴 (结构). 它们也有一个名字(属性)并且会叫(行为).

  • 所有 “Computer(计算机)” 都有一颗CPU和一些内存(结构), 一个型号名称(属性), 能开机和关机(行为).

我们定义一个类用来研究基于类的编程的基础.我们从 "Square(正方形)"类开始,它代表一个有着简单方法用于计算它的面积的正方形.

你可以按下面的语法来定义这个正方形类:

// 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());

这是一个使用 Ext JS 实现一个类的骨架.虽然它完全满足我们代表一个正方形并且提供了一个计算面积的方法的目标,但仍不是一个理想的或者说好的实践.

构造器

让我们使用一个构造器来提升这个例子. 一个构造器是当一个类实例化时被调用的特殊函数.首先,让我们修改我们设置正方形边长值的方式.通过使用构造器,我们能删除上面例子中"难看"的行.

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());

如果你要传递2个或以上的属性值给构造器,你可以象如下一样使用对象:

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(' ')
 );

应用

我们能通过使用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(' ')
 );

定义多个类

让我们创建一个圆和矩形类来说明一些小的差异.

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 />'));

继承

在一头扎进继承这个概念前, 我们回顾一下下面的例子. 你可以在下面看到,我们已经给正方形类增加了一个特别的方法然后修改产生测试消息的方式:

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));

下一个例子中,我们也对长方形和圆形类执行同样的修改:

Ext.define('Square', {
     side: 0,
     color: 'red',
     border: true,

     constructor: function(config) {
         Ext.apply(this, config);
     },

     getArea: function() {
         return this.side * this.side;
     },

     getShapeName: function () {
         return 'square';
     }
 });

 Ext.define('Rectangle', {
     base: 0,
     height: 0,
     color: 'green',
     border: true,

     constructor: function(config) {
         Ext.apply(this, config);
     },

     getArea: function() {
         return this.base * this.height;
     },

     getShapeName: function () {
         return 'rectangle';
     }
 });

 Ext.define('Circle', {
     radius: 0,
     color: 'blue',
     border: true,

     constructor: function(config) {
         Ext.apply(this, config);
     },

     getArea: function() {
         return Math.PI.toFixed(2) * Math.pow(this.radius, 2);
     },

     getShapeName: function () {
         return 'circle';
     }
 });

 // Generates a sentence that will be displayed 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
         }),
     rectangle = Ext.create('Rectangle', {
             base: 4,
             height: 3
         }),
     circle = Ext.create('Circle', {
             radius: 3
         });

 Ext.Msg.alert('Message', [
     generateTestSentence(square),
     generateTestSentence(rectangle),
     generateTestSentence(circle)
 ].join('<br />'));

如果你仔细查看上面的例子,你可能会注意到很多重复代码.这让你的代码验证以维护和导致错误.继承的概念帮助我们合并重复代码和让它更易理解和维护.

父类和子类

为应用继承概念,我们通过给一个父类的子类们属性来简单化和减少重复代码:

// 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() 方法移到形形状类中:

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 />'));

你可以看到,如果父类和子类中设置了相同的属性则子类中的属性会覆写中的属性.例如,形状中的 shapeName 是 “shape”. 而在正方形类中 shapeName 也做了设置,它覆盖了父类的值.如果子类没有设置属性值,就会继承父类中的值.

封装

在上一个例子中,你可能注意到我们通过直接调用来访问实例的属性. 例如,通过访问“square.color”获取正方形的颜色. 你也能直接设置属性的值:

Ext.define('Shape', {
     color: 'gray',
     border: true,
     shapeName: 'shape',

     constructor: function (config) {
         Ext.apply(this, config);
     },

     getShapeName: function () {
         return this.shapeName;
     },

     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
 });

 // Set the value of 'side' to 5 instead of the initial 4 
 // While not bad, this is something that should be avoided
 square.side = 5;

 // Set the value of 'side' to a string instead of a number
 // String is not a valid value. This is an example of why 
 // direct access to the properties should be avoided.  
 // Open access is prone to error.
 square.side = 'five';

 // The area will be reported as NaN
 Ext.Msg.alert('Message', 
               [ square.getTestSentence() ].join('<br />'));

配置块

为了避免直接对一个对象的属性进行读/写,我们将使用 Ext JS的 config 块. 这个将自动限制访问对象的属性,这样它们只能通过访问方法来设置和获取.

访问方法自动为一个类中的配置块生成getter和setter.例如,如果你在配置块中有一个 shapeName 你可以默认使用 setShapeName()getShapeName().

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.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 JS中, 除非显示地指定所有类都一个一个共同基础类的子类.这个基础类是 Ext.Base.

就象我们的正方形类继承自形状,形状类自动继承自 Ext.Base.
基于这一逻辑,下面的代码:

Ext.define('Shape', {
     // Properties and methods here
 });

完全等同于这个:

Ext.define('Shape', {
     extend: 'Ext.Base'
     // Properties and methods here
 });

这就是为什么我们可以在形状的构造器中使用 this.initConfig(config); . initConfig()是 Ext.Base 的一个方法并且被所有继承自它的子类继承. initConfig() 为它所在的类初始化config 块并且自动生成访问方法.

实际属性封装

封闭的主要目的是保护对象不被不必要或无效的属性修改.这些修改可能必然的导致错误.

例如,当使用 config 块来避免直接属性修改, 现在没有办法可以防止给访问方法传递无效的值.就是说没有方法防止我们square.setSide(‘five’)这样的调用,这个会导致错误,因为边长要求是一个数字.

让我们使用apply方法来避免.apply 是一个允许你在修改前测试赋值的模版方法.这个方法复制这个特定对象 config 中的所有属性.

因为 ‘side’ 是通过 ‘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 />'));

结论

我们希望这个向导阐述清楚了在Ext JS中的面象对象OOP和基于类的编程的基本概念.在开发你自己应用时,确定再看看 类系统 向导 来获得更多关于如何厘清 Ext JS的类系统的知识.一如既往的, 如果你对向导中的内容有任何疑问,请在 community forums(社区论坛) 中提问或者通过在 Support Portal(支持平台) 上提交支持单(只有Sencha 支持客户 可以访问).

Last updated