《Ext JS权威指南》——2.7节Ext JS 4语法

简介: 本节书摘来自华章社区《Ext JS权威指南》一书中的第2章,第2.7节Ext JS 4语法,作者:黄灯桥,更多章节内容可以访问云栖社区“华章社区”公众号查看

2.7 Ext JS 4语法
1.配置对象
Ext JS的基本语法就是使用树状的配置对象来定义界面,其格式如下:

{
      config_options1:value1,
    config_options1:value2,
    …
     config_optionsn:valuen,
    layout:{},
    items:[
    {},//配置对象
    {}//配置对象
    …
  ],
    listeners:{
    //定义事件(根据需要而定)
        click:function(){},
        dblclick:function(){}
        …
    }
}

格式中从config_options1、config_options2到config_optionsn都是API文档中对象的配置项(config options)。很多初学者会错误地认为API不全,找不到配置项。事实上API是完整的,只是有些布局隐含了面板或其他部件,但都在同一配置对象内定义。例如在使用Accordion布局的时候,想消除布局标题栏右边的小图标,需要使用hideCollapseTool属性,而该属性是在Panel对象里的。在这方面,Ext JS 4的API已经做了调整了,增加了一个其他配置项(Other Configs)的列表。
属性layout可以是对象,也可以是字符。该属性表示在当前容器内使用什么布局来填充子控件。如果没有特殊要求,直接使用布局的别名作为值,例如,2.3节的示例中Viewport使用了Fit布局来放置子控件。如果有特殊要求,则要使用对象来定义该值。例如,如果使用Hbox布局,布局内的子控件需要居中对齐,则定义如下:

layout:{
    type:'hbox',
    align:'middle'
}

属性items是一个数组,可以在里面定义当前控件的子控件,里面可以是1个或多个配置项,根据你的需要而定。例如2.3节的示例,在Viewport下使用了一个Panel面板作为其面板。属性listeners是一个对象,可以在里面绑定事件,对象的属性就是事件名称,属性值就是要执行的函数。
2.关于xtype
在使用Ext JS编写应用时,很多时候通过定义xtype来指定该位置使用什么组件,例如2.3节的示例中的“xtype:"panel"”,这里指定使用面板作为当前位置的组件。这样做的主要目的是简化代码。如果没有xtype,就得先使用变量指向一个组件,然后将其加入父组件中,或者直接在父组件的定义中加入一堆由new关键或Ext.Create方法创建的组件。这不但影响了书写代码的效率,还影响了代码的可读性。有xtype存在,就不用担心这些问题了。
在某些组件下,会默认其内部组件为某些组件,因而也就不需要书写xtype语句。例如,在2.3节的示例中把xtype去掉,代码也能正常运行,因为面板是Viewport内默认的组件。一般来说,没特别声明,使用的都是Panel组件。
定义xtype,一般使用组件的别名。可在API文档的组件说明文档的顶部或Component对象的说明中找到组件的xtype值。

  1. 使用new关键字创建对象
    在Ext JS 4版本之前,一直使用new关键字创建对象,其语法如下:

new classname([config])
其中calssname是指类名;config是可选参数,为类的配置对象(config options),类型为JSON对象。在2.3节的示例中,Ext.Viewport就是使用该方法创建的。
4.使用Ext.create方法创建对象
Ext.create方法是新增的创建对象的方法,其语法如下:

Ext.create(classname,[config])

其中classname可以是类的全名、别名或备用名;config是可选参数,为类的配置对象(config options),类型为对象。将2.3节的示例中的以下代码:

new Ext.Viewport({
修改为:
Ext.create('Ext.Viewport',{

效果是一样的。那为什么要增加这样一个方法呢?我们来研究一下create方法的源代码。
在ClassManger.js文件中,可以找到以下代码:

Ext.apply(Ext, {
    create: alias(Manager, 'instantiate'),
        ...    
})
从代码可知create方法其实是Ext.ClassManager类的instantiate方法的别名,其代码如下:
instantiate: function() {
    var name = arguments[0],
        nameType = typeof name,
        args = arraySlice.call(arguments, 1),
        alias = name,
        possibleName, cls;

    if (nameType != 'function') {
        if (nameType != 'string' && args.length === 0) {
            args = [name];
            name = name.xclass;
        }

        //省略调试代码
        cls = this.get(name);
    }
    else {
        cls = name;
    }
    if (!cls) {
        possibleName = this.getNameByAlias(name);

        if (possibleName) {
            name = possibleName;

            cls = this.get(name);
        }
    }

    if (!cls) {
        possibleName = this.getNameByAlternate(name);

        if (possibleName) {
            name = possibleName;

            cls = this.get(name);
        }
    }

    if (!cls) {
        //省略调试代码

        Ext.syncRequire(name);

        cls = this.get(name);
    }
    //省略调试代码
    return this.getInstantiator(args.length)(cls, args);
},

首先将name变量指向从参数中获取的类名,然后判断name是不是函数,如果不是,且其不是字符串,则从xclass属性中获取类名。接着使用get方法获取对象,并将cls变量指向对象。
get方法的源代码如下:

get: function(name) {
    var classes = this.classes;
    if (classes[name]) {
        return classes[name];
    }
    var root = global,
        parts = this.parseNamespace(name),
        part, i, ln;
    for (i = 0, ln = parts.length; i < ln; i++) {
        part = parts[i];
        if (typeof part != "string") {
            root = part;
        } else {
            if (!root || !root[part]) {
                return null;
            }
            root = root[part];
        }
    }
    return root;
},

代码中的classes对象包括了Ext JS的所有类,因而代码首先根据name从classes中寻找类,如果存在,则返回对象;如果不存在,则说明是用户自定义的类,需要从Ext.global中寻找。
查找工作首先要做的是使用parseNamespace方法拆解类名,其代码如下:

parseNamespace: function(namespace) {
        //省略调试代码
    var cache = this.namespaceParseCache;

    if (this.enableNamespaceParseCache) {
        if (cache.hasOwnProperty(namespace)) {
            return cache[namespace];
        }
    }

var parts = [],
    rewrites = this.namespaceRewrites,
    root = global,
    rewrite, from, to, i, ln;

    for (i = 0, ln = rewrites.length; i < ln; i++) {
        rewrite = rewrites[i];
        from = rewrite.from;
        to = rewrite.to;

        if (namespace === from || namespace.substring(0, from.length) === from) {
            namespace = namespace.substring(from.length);

            if (typeof to !== "string") {
                root = to;
            } else {
                parts = parts.concat(to.split("."));
            }

            break;
        }
    }

    parts.push(root);

    parts = parts.concat(namespace.split("."));

    if (this.enableNamespaceParseCache) {
        cache[namespace] = parts;
    }

    return parts;
},

代码中namespaceParseCache对象的作用是使用类名作为关键字并指向拆解后的类名数组,这样,当该类被多次使用时,就可以直接从namespaceParseCache对象中获取拆解的类名数组,而不需要再拆解一次,从而加快运行速度。
通过enableNamespaceParseCache属性可配置是否开启namespaceParseCache对象的缓存功能,默认是开启的。如果在enableNamespaceParseCache中已存在类名的拆解结果,则返回结果。
属性namespaceRewrites的定义如下:

namespaceRewrites: [{
    from: 'Ext.',
    to: Ext
}],

在其他文件中找不到为namespaceRewrites添加数据的代码,因而在循环中,如果类名是以“Ext.”开头的,root会指向Ext对象;如果不是,root就是初始值,指向Ext.global。
接着,将root指向的对象存入parts数组,再将拆分类名产生的数组与parts数组合并。如果开启了缓存功能,在namespaceParseCache对象中,将以类名为关键字指向parts数组,最后返回parts数组。数组返回后,通过循环的方式来查找类定义。因为返回数组(parts)的第1个数据不是Ext对象就是Ext.golbal对象,因而变量root在第一次循环时,会指向Ext对象或Ext.golbal对象。在后续循环中,会根据拆分的类名在Ext或Ext.gdbal对象中逐层找下去。如果期间“root[part]”不是对象,说明不存在该类,返回null。如果找到了,返回root指向的对象。
如果cls不是指向对象,则尝试使用别名方法查找对象。查找时,首先使用getName-ByAlias方法将别名转换为类名,其代码如下:

getNameByAlias: function(alias) {
        return this.maps.aliasToName[alias] || '';
},
在maps属性中保存了以下3个对象:
maps: {
    alternateToName: {},
    aliasToName: {},
    nameToAliases: {}
},

简单来说,maps中的对象就是一个对照表,通过它就可轻松地找到类名和别名。对象alternateToName的作用是通过备用名获得类名,它以备用名作为关键字、类名作为值;对象aliasToName的作用是通过别名获取类名,它以别名作为关键字、类名作为值;对象nameToAliases的作用是通过类名获取别名,它以类名作为关键字、别名作为值。在创建类的时候,会把类名、别名和备用名写入对照表。
如果getNameByAlias方法返回的不是空字符串,说明变量name保存的是别名,需将name修改为类名,然后通过get方法获取对象。如果使用别名也找不到对象,则可尝试使用备用名来查找对象,执行代码与使用别名查找的方式类似。如果还没有找到,则可尝试使用syncRequire方法下载对象,其代码如下:

syncRequire: function() {
    this.syncModeEnabled = true;
    this.require.apply(this, arguments);
    this.refreshQueue();
    this.syncModeEnabled = false;
},

在上面的代码中,syncModeEnabled方法可控制Loader对象的require方法通过同步的方式去下载对象。
如果实在是找不到,就会抛出异常。最后使用getInstantiator方法实例化对象,其代码如下:

getInstantiator: function(length) {
    if (!this.instantiators[length]) {
        var i = length,

            args = [];

        for (i = 0; i < length; i++) {
            args.push('a['+i+']');
        }

        this.instantiators[length] = new Function('c', 'a', 'return new c('+args.join(',')+')');
    }

    return this.instantiators[length];
},

代码中的数组instantiators起缓存作用,因为同样的参数长度产生的匿名函数是一样,没必要每次都重新创建一次。最后返回的匿名函数如下:
(function anonymous(c, a) {return new c(a[0], a[1], … ,a[n]);})
代码中的n就是参数的长度,例如length的值为5,则匿名函数如下:
(function anonymous(c, a) {return new c(a[0], a[1], a[2], a[3], a[4]);})
匿名函数返回后会立即执行,参数c指向对象的cls,a指向实例化对象时的参数。也就是说,对象是在匿名函数中实例化的,这样可以保证对象的作用域是安全的。
从以上的分析可以了解到,创建对象的新方法不但可以实现动态加载,而且可以保证作用域的安全,应该优先使用。
5.使用Ext.widget或Ext.createWidget创建对象
Ext.widget的作用是使用别名来创建对象,其语法与2.7.2节介绍的Ext.create是一样的,只是classname使用的是对象的别名。Ext.createWidget是Ext.widget的另外一种使用方法而已,在源代码中的定义如下:

Ext.createWidget = Ext.widget;
Ext.widget的代码如下:
widget: function(name) {
    var args = slice.call(arguments);
    args[0] = 'widget.' + name;

    return Manager.instantiateByAlias.apply(Manager, args);
},
也就是说,它使用ClassManager对象的instantiateByAlias方法创建对象,其代码如下:
instantiateByAlias: function() {
    var alias = arguments[0],
        args = slice.call(arguments),
        className = this.getNameByAlias(alias);

    if (!className) {

        className = this.maps.aliasToName[alias];

        //省略调试代码

        Ext.syncRequire(className);
    }

    args[0] = className;

    return this.instantiate.apply(this, args);
},

代码先是根据别名寻找类名,如果找不到就抛出异常或尝试使用syncRequire方法加载类文件,最后调用instantiate方法创建对象。
6.使用Ext.ns 或 Ext.namespace定义命名空间
在使用C#或Java做开发时,很多时候都会使用命名空间来组织相关类和其他类型。在JavaScript中并没有提供这样的方式,不过可以通过定义一个全局对象的方式来实现,譬如你要定义一个“MyApp”的命名空间,你可以这样:

MyApp ={};

这里要注意,不要使用var语句去定义全局对象。
在Ext JS中,使用Ext.namespace方法可创建命名空间,其语法如下:
Ext.namespace(namespace1,namespace2,…,namespacen)
其中namespace1、namespace2和namespacen都是字符串数据,是命名空间的名字,例如:

//推荐用法
Ext.namespace("MyApp","MyApp.data","MyApp.user");
Ext.ns("MyApp","MyApp.data","MyApp.user");
//或者,不建议使用
Ext.namespace(,"MyApp.data","MyApp.user");
Ext.ns("MyApp.data","MyApp.user");

Ext.ns只是Ext.namespace的简单写法。
在ClassManager.js中可找到Ext.namespace的createNamespace方法的实现代码:

createNamespaces: function() {
  var root = global,
      parts, part, i, j, ln, subLn;

  for (i = 0, ln = arguments.length; i < ln; i++) {
      parts = this.parseNamespace(arguments[i]);

      for (j = 0, subLn = parts.length; j < subLn; j++) {
          part = parts[j];

          if (typeof part !== 'string') {
              root = part;

          } else {
              if (!root[part]) {
                  root[part] = {};
              }

              root = root[part];
          }
      }
  }

  return root;
},

从上面代码可以看到,Ext.namespace创建的命名空间是保存在global对象里的。注意循环结构,数组长度都是先保存到一个局部变量再使用的,原因是JavaScript与C#、Java等语言不同,每次循环都要计算一次数组长度,这样会降低性能。在第一个循环下,使用了parseNamespace方法拆分命名空间的字符串。
在Ext JS初始化时,this指向的是window对象,因而global指向的是window对象。
数组返回到createNamespace方法后,开始执行完循环,最后在window对象下生成了以下结构的全局对象:

{
  MyApp:{
      Data:{}
  }
}

虽然生成的是全局对象,但是在作用域链上,它可以直接在Ext JS的作用域链内搜索对象,而不需要到最外层的全局作用域链搜索对象,这是最大的不同。
在任何编程语言里都不提倡使用全局变量,尤其是在JavaScript里,全局对象在每一层的作用域链里都搜索不到时,才会在全局作用域链里搜索,效率相当低。因此,在JavaScript里不仅不推荐使用全局变量,而且建议当有一个变量不是在当前作用域定义的,并要多次使用时,建议将该变量保存在局部变量中,使用局部变量来进行操作,以避免在域链中多次搜索对象而降低性能。
因此,建议的方法是在Ext对象下创建命名空间,譬如创建以下的命名空间:

Ext.ns("Ext.MyApp","Ext.MyApp.data","Ext.MyApp.user");

最好的方法是使用Ext已定义的命名空间“Ext.app”,如果是扩展或插件则使用“Ext.ux”。
7.使用Ext.define定义新类
在Ext JS 3中,定义新类使用的是Ext.extend方法,因为Ext JS 4的类系统进行了重新架构,所以定义新类的方法也修改为了Ext.define方法,其语法如下:

Ext.define(classname,properties,callback);

其中:
classname:要定义的新类的类名。
properties:新类的配置对象,对象里包含了类的属性集对象,表2-1列出了常用的属性及其说明。
callback:回调函数,当类创建完后执行该函数。


b5c97b874229811e8ead79e69e7e331c9a8c1003


47e1d2111dd23c04dce74f43c841aa8b6c867d0c


4a47c91008ef392012428c76d403dd11734c7ec6

代码中,initConfig方法执行后就会为congfig的属性创建set和get方法,这样创建类的示例后,就可以直接通过set和get方法读写属性的值,譬如例子中用setWidth设置了配置中width的值,使用getHeight获取height的值

 定义静态方法,例如:
Ext.define("subclass",{    
      statics:{
          method:function(args){
              return new this(args);
        }
statics      },
           constructor:function(config){
          this.initConfig(config);
          …    
      },
      …
    });
    var myclass=subclass.method("class");

类的创建过程将在第4章讲解,下面我们来实践一下。在Firefox中打开2.3节的示例,然后打开Firebug,在控制台中输入以下代码:

Ext.define("Calculator",{
    constructor:function(){
        return this;
    },
    plus:function(v1,v2){
        return v1+v2;
    },
    minus:function(v1,v2){
        return v1-v2;
    },
    multiply:function(v1,v2){
        return v1*v2;
    },
    divid:function(v1,v2){
        return v1/v2
    }
});
var cal=new Calculator();
console.log(cal.plus(87,28)); //115
console.log(cal.minus(87,28)); //59
console.log(cal.multiply(7,8)); //56
console.log(cal.divid(28,7)); //4

代码中定义了一个名称为“Calculator”的类,它包含加、减、乘、除4个方法。运行后,将Firebug标签页切换到DOM标签,可看到如图2-2所示的Calculator类。


d61a56c09bfa5b30d3f2323c615524d8a399da98

继续创建一个继承自Calculator类的新类NewCalculator,新类添加了十进制转换为十六进制的方法,代码如下:

Ext.define('NewCalculator',{
    extend:'Calculator',
    hex:function(v1){
        return v1.toString(16);
    }
});
var ncal=new NewCalculator();
console.log(ncal.hex(28)); //1c

代码中extend属性的值为Calculator,表示NewCalculator将继承自Calculator类。方法hex是新增的进制转换方法。
运行后可在DOM树中看到如图2-3所示的NewCalculator类。


8c9034aab64ae3f0c75c9b550dc8f0369d046f9c

比较一下图2-3与图2-2,可看到Calculator类的超类(superclass)是Ext.Base,而NewCalculator类的超类是Calculator,这说明,没有使用extend属性定义的类会默认从Ext.Base中继承。

继续我们的实践,这次要做的是将HEX、BIN、OCT这3个实现不同进制转换的类混合到NewCalculator类中。第一步要做的是定义3个实现进制转换的类:

Ext.define('HEX',{
    hex:function(v1){
        return v1.toString(16);
    }
});
Ext.define('BIN',{
    bin:function(v1){
        return v1.toString(2);
    }
});
Ext.define('OCT',{
    oct:function(v1){
        return v1.toString(8);
    }
});
然后将HEX、BIN和OCT三个功能混合到继承自Calculator类的NewCalculator类中,这样NewCalculator除了实现加减乘除功能外,还能实现十进制到二进制、八进制和十六进制的转换,代码如下:
Ext.define('NewCalculator',{
    extend:'Calculator',
    mixins:{
        Hex:'HEX',
        Bin:'BIN',
        Oct:'OCT'
    },
    convert:function(value,type){
        switch(type){
            case 2:
                return this.bin(value)
                break;
            case 8:
                return this.oct(value)
                break;
            default:
                return this.mixins.Hex.hex.call(this,value);
                break;
        }
    }
});


var ncal=new NewCalculator();
console.log(ncal.convert(25,2)); //11001
console.log(ncal.convert(25,8)); //31
console.log(ncal.convert(25,16)); //19

在代码中,使用convert方法可进行进制转换,第2个参数type表示要转换的进制类型。
调用mixins属性定义的混合功能有两种方法:第一种是二进制和八进制中使用的直接调用的方法,第二种是十六进制中使用call方法调用的方法。
运行后,在DOM中可看到如图2-4所示的NewCalculator类。


6b62a60d9f5405e88f394088abeb1d1d488edaf1

在图2-4中的原型(prototype)节点里,可看到mixins对象中的3个属性都指向了对应的BIN、HEX和OCT对象,而这三个对象中的方法也直接附加到原型上了,这也就解释了为什么可以使用两种方法调用混合功能的方法。要注意的是,如果存在同名方法,譬如将BIN、HEX和OCT定义的方法都修改为convert,那么使用直接调用的方法就会出现错误,因为在JavaScript中,存在同名函数,前面的定义会被最后定义的函数覆盖,所以在不确定是否有同名方法的情况下,建议还是使用第二种call的方法。
定义3个进制转换类实在麻烦,在一个Convert类中,预设好一个type参数,然后根据type定义的类型进行转换就方便多了,代码如下:

Ext.define("Convert",{
    config:{
    type:"hex"
    },
    type_num:16,
    constructor:function(config){
        this.initConfig(config);
        return this;
    },
    applyType:function(type){
        this.type_num= type=="hex" ? 16 : ( type=="oct" ? 8 : 2);
        return type;
    },
    convert:function(v){
        return v.toString(this.type_num);
    }
});
var cv=new Convert();
console.log(cv.convert(29)); //1d
cv.setType("oct");
console.log(cv.convert(29)); //35
cv.setType("bin");
console.log(cv.convert(29)); //11101

在Convert类中,type属性在initConfig执行后会自动生成applyType、setType、getType和resetType这4个方法。当type的值发生改变时,会触发applyType方法,因而可以重写applyType方法以实现所需的功能。在applyType方法中,根据type的值修改了type_num的值,这样在convert方法中就可以使用type_num值直接进行进制转换了。
代码运行后在DOM中可看到如图2-5所示的Convert类。


a524f76162348427734728f56c2e603f516820c8

在图2-5的原型中,可看到与Type有关的4个方法。实现这么简单的功能,每次都要使用new关键字对象太麻烦了。如果是静态类就好了,所以修改代码如下:

Ext.define("Convert",{
    statics:{
        hex:function(v){
            return v.toString(16);
        },
        oct:function(v){
            return v.toString(8);
        },
        bin:function(v){
            return v.toString(2);
        }
    },
    constructor:function(config){
        return this;
    }
});
console.log(Convert.hex(29)); //1d
console.log(Convert.oct(29)); //35
console.log(Convert.bin(29)); //11101

在Convert类中,3个进制转换方法都定义在statics属性中。运行后在DOM树中可看到如图2-6所示的Convert类。


52df3de5f80486e5809249a8407487be5c3ce4e6

在图2-6中,3个进制转换方法不在原型里,而是直接挂在类节点下。现在大家应该清楚静态类和其他类的区别了。

相关文章
|
2月前
|
JavaScript 前端开发
关于 JavaScript 代码里双重感叹号的语法
关于 JavaScript 代码里双重感叹号的语法
46 1
|
3月前
|
JavaScript 前端开发
JavaScript基础语法(类型转换)
JavaScript基础语法(类型转换)
27 0
|
3月前
|
JavaScript 前端开发 Java
JavaScript基础语法(流程控制语句)
JavaScript基础语法(流程控制语句)
25 0
|
3月前
|
JavaScript 前端开发 Java
JavaScript基础语法(运算符)
JavaScript基础语法(运算符)
32 0
|
3月前
|
JavaScript 前端开发 Java
JavaScript基础语法(变量)
JavaScript基础语法(变量)
48 0
|
19天前
|
JavaScript 前端开发 Web App开发
JavaScript基础语法(codewhy版本)(一)
JavaScript基础语法(codewhy版本)
83 1
JavaScript基础语法(codewhy版本)(一)
|
1月前
|
JavaScript 前端开发 网络架构
JavaScript的数组教程(最详细,更新至es6新语法)
JavaScript的数组教程(最详细,更新至es6新语法)
|
1月前
|
JavaScript 前端开发 Java
Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(中)
Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(中)
|
1月前
|
JavaScript 前端开发 Java
Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(上)
Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(上)
|
2月前
|
存储 JavaScript 前端开发
【JavaEE初阶】 JavaScript基础语法——贰
【JavaEE初阶】 JavaScript基础语法——贰