用函数模板,写一个简单高效的 JSON 查询器的方法介绍

所属分类: 网络编程 / JavaScript 阅读数: 1422
收藏 0 赞 0 分享

JSON可谓是JavaScript的亮点,它能用优雅简练的代码实现Object和Array的初始化。同样是基于文本的数据定义,它比符号分隔更有语义,比XML更简洁。因此越来越多的JS开发中,使用它作为数据的传输和储存。

JS数组内置了不少有用的方法,方便我们对数据的查询和筛选。例如我们有一堆数据:

复制代码 代码如下:

var heros = [
        // 名============攻=====防=======力量====敏捷=====智力====
        {name:'冰室女巫', DP:38, AP:1.3, Str:16, Agi:16, Int:21},
        {name:'沉默术士', DP:39, AP:1.1, Str:17, Agi:16, Int:21},
        {name:'娜迦海妖', DP:51, AP:6.0, Str:21, Agi:21, Int:18},
        {name:'赏金猎人', DP:39, AP:4.0, Str:17, Agi:21, Int:16},
        {name:'剧毒术士', DP:45, AP:3.1, Str:18, Agi:22, Int:15},
        {name:'光之守卫', DP:38, AP:1.1, Str:16, Agi:15, Int:22},
        {name:'炼金术士', DP:49, AP:0.6, Str:25, Agi:11, Int:25}
        //...
    ];

要查询攻击大于40并且防御小于4的英雄,我们可以用Array的filter方法:

复制代码 代码如下:

 var match = heros.filter(function(e) {
        return e.DP > 40 && e.AP < 4;
    });

返回得到一个数组,包括符合条件的2个结果。

相比手工去写循环判断,filter方法为我们提供了很大的方便。但它是基于函数回调的,所以每次使用必须写一个function,对于简单的查询很是累赘,而且使用回调效率也大大降低。但这是也没有办法的,想简单必然要牺牲一定性能。 如果能使用比这更简单的语句,并且完全拥有代码展开时效率,该有是多么完美的事。

先来想象下,要是能将上面的代码写成这样,并且查询速度和手写的遍历判断一样

复制代码 代码如下:

    var match = heros.select('@DP>40 AND @AP<4');

看上去有点像SQL,连语法都换了?这样岂不是要写一个词法分析,语义解释等等等等一大堆的脚本引擎的功能了,没个几千上万行代码都搞不定,而且效率肯定更糟了。。。如果想到那么复杂,那么你还没深刻的理解脚本的精髓。但凡是脚本语言,都有运行时动态解释代码的接口,例如vbs的execute();js的eval(),new Function(),甚至创建一个<script>动态写入代码。

显然,要是能将另一种语言,翻译成js代码,那么就可直接交给宿主来执行了!

例如上面select中的字符,我们简单的将"@"替换成"e.", "AND"替换成"&&",于是就成了一个合法的js表达式,完全可以交给eval来执行。

所以我们要做的,就是将原始语句翻译成js语句来执行。并且为了提高效率,将翻译好的js表达式内联到一个上下文环境,生成一个可执行的函数体,而不是每次遍历中都依靠回调来判断。

于是,函数模版就要派上用场了。

函数模版简介

在C++里面,有宏和类模版这么个东西,可以让一些计算在编译阶段就完成了,大幅提升了运行时代码的性能。脚本虽然没有严格意义上的编译,但在第一次执行的时候会解析并充分的优化,这是目前主流浏览器相互竞争点。所以,我们要将重复eval的代码,镶嵌到事先提供的样板函数里:一个准备就绪,就差表达式计算的函数:

复制代码 代码如下:

 /**
     * 模版: tmplCount
     * 功能: 统计arr数组中符合$express表达式的数量
     */
    function tmplCount(arr) {
        var count = 0;

        for(var i = 0; i < arr.length; i++) {
            var e = arr[i];

            if($express) {
                count++;
            }
        }
        return count;
    }


上面就是一个模板函数,遍历参数arr[]并统计符合$express的数量。除了if(...)内的表达式外,其他都已经准备就绪了。字符$express也可以换成其他标识,只要不和函数内其他字符冲突即可。

当我们需要实例化时,首先通过tmplCount.toString()将函数转成字符串格式,然后将其中的$express替换成我们想要的表达式,最后eval这串字符,得到一个Function类型的变量,一个模板函数的实例就产生了!

我们简单的演示下:

复制代码 代码如下:

 /**
     * 函数: createInstance
     * 参数: exp
     *      一段js表达式字符串,用来替换tmplCount模板的$express
     * 返回:
     *      返回一个Function,模版tmplCount的实例
     */
    function createInstance(exp)
    {
        // 替换模板内的表达式
        var code = tmplCount.toString()
                    .replace('$express', exp);

        // 防止匿名函数直接eval报错
        var fn = eval('0,' + code);

        // 返回模板实例
        return fn;
    }


    // 测试参数
    var student = [
        {name: 'Jane', age: 14},
        {name: 'Jack', age: 20},
        {name: 'Adam', age: 18}
    ];

    // demo1
    var f1 = createInstance('e.age<16');
    alert(f1(student));    //1个

    // demo2
    var f2 = createInstance('e.name!="Jack" && e.age>=14');
    alert(f2(student));    //2个


注意createInstance()的参数中,有个叫e的对象,它是在tmplCount模版中定义的,指代遍历时的具体元素。返回的f1,f2就是tmplCount模板的两个实例。最终调用的f1,f2函数中,已经内嵌了我们的表达式语句,就像我们事先写了两个同样功能的函数一样,所以在遍历的时候直接运行表达式,而不用回调什么的,效率大幅提升。

其实说白了,tmplCount的存在仅仅是为了提供这个函数的字符串而已,其本身从来不会被调用。事实上用字符串的形式定义也一样,只不过用函数书写比较直观,方便测试。

值得注意的是,如果脚本后期需要压缩优化,那么tmplCount模板绝对不能参与,否则对应的"e."和"$express"都有可能发生变化。

JSON基本查询功能

函数模板的用处和实现介绍完了,再来回头看之前的JSON查询语言。我们只需将类似sql的语句,翻译成js表达式,并且生成一个函数模板实例。对于相同的语句,我们可以进行缓存,避免每次都翻译。

首先我们实现查询器的模板:

复制代码 代码如下:

 var __proto = Object.prototype;

    //
    // 模板: __tmpl
    // 参数: $C
    // 说明: 记录并返回_list对象中匹配$C的元素集合
    //
    var __tmpl = function(_list) {
        var _ret = [];
        var _i = -1;

        for(var _k in _list) {
            var _e = _list[_k];

            if(_e && _e != __proto[_k]) {
                if($C)
                    _ret[++_i] = _e;
            }
        }
        return _ret;

    }.toString();


然后开始写Object的select方法:
复制代码 代码如下:

 //
    // select方法实现
    //
    var __cache = {};

    __proto.select = function(exp) {
        if(!exp)
            return [];

        var fn = __cache[exp];

        try {
            if(!fn) {
                var code = __interpret(exp);            //解释表达式
                code = __tmpl.replace('$C', code);      //应用到模版

                fn = __cache[exp] = __compile(code);    //实例化函数
            }

            return fn(this);                            //查询当前对象
        }
        catch(e) {
            return [];
        }
    }


其中__cache表实现了查询语句的缓存。对于重复的查询,性能可以极大的提升。
复制代码 代码如下:

    function __compile() {
        return eval('0,' + arguments[0]);
    }

 __compile之所以单独写在一个空函数里,就是为了eval的时候有个尽可能干净的上下文环境。

__interpret是整个系统的重中之重,负责将查询语句翻译成js语句。它的实现见智见仁,但尽可能简单,不要过度分析语法。

具体代码查看:jsonselect.rar
出于演示,目前只实现部分基本功能。以后还可以加上 LIKE,BETWEEN,ORDER BY 等等常用的功能。

Demo

复制代码 代码如下:

var heros = [
        // 名============攻=====防=======力量====敏捷=====智力====
        {name:'冰室女巫', DP:38, AP:1.3, Str:16, Agi:16, Int:21},
        {name:'沉默术士', DP:39, AP:1.1, Str:17, Agi:16, Int:21},
        {name:'娜迦海妖', DP:51, AP:6.0, Str:21, Agi:21, Int:18},
        {name:'赏金猎人', DP:39, AP:4.0, Str:17, Agi:21, Int:16},
        {name:'剧毒术士', DP:45, AP:3.1, Str:18, Agi:22, Int:15},
        {name:'光之守卫', DP:38, AP:1.1, Str:16, Agi:15, Int:22},
        {name:'炼金术士', DP:49, AP:0.6, Str:25, Agi:11, Int:25}
        //...
    ];

复制代码 代码如下:

 // 查询:力量,敏捷 都超过20的
    // 结果:娜迦海妖
    var match = heros.select('@Str>20 AND @Agi>20');

    // 查询:“士”结尾的
    // 结果:沉默术士,剧毒术士,炼金术士
    var match = heros.select('right(@name,1)="士" ');

    // 查询:生命值 超过500的
    // 结果:炼金术士
    var match = heros.select('100 + @Str*19 > 500');


更多精彩内容其他人还在看

jQuery LigerUI 使用教程表格篇(1)

ligerGrid是ligerui系列插件的核心控件,用户可以快速地创建一个美观,而且功能强大的表格,支持排序、分页、多表头、固定列等等
收藏 0 赞 0 分享

JavaScript中常用的运算符小结

JavaScript中常用的运算符小结,需要的朋友可以参考下。
收藏 0 赞 0 分享

深入理解JavaScript系列(13) This? Yes,this!

在这篇文章里,我们将讨论跟执行上下文直接相关的更多细节。讨论的主题就是this关键字。实践证明,这个主题很难,在不同执行上下文中this的确定经常会发生问题
收藏 0 赞 0 分享

javascript (用setTimeout而非setInterval)

javascript (用setTimeout而非setInterval)如果用setInterval 可能出现 下次调用会在前一次调用前调用
收藏 0 赞 0 分享

JavaScript中两个感叹号的作用说明

用两个感叹号的作用就在于,如果明确设置了o中flag的值(非null/undefined/0""/等值),自然test就会取跟o.flag一样的值;如果没有设置,test就会默认为false,而不是null或undefined
收藏 0 赞 0 分享

javascript写的简单的计算器,内容很多,方法实用,推荐

最近用javascript写了一个简单的计算器,自己测试感觉还好,代码都给了注释,非常不错,推荐大家学习。
收藏 0 赞 0 分享

js的表单操作 简单计算器

javascript写的简单的加减乘除计算器,里面涉及到一些方法还是很实用的哦,新手不要错过
收藏 0 赞 0 分享

Jquery中删除元素的实现代码

empty用来删除指定元素的子元素,remove用来删除元素,或者设定细化条件执行删除
收藏 0 赞 0 分享

javaScript 利用闭包模拟对象的私有属性

JavaScript缺少块级作用域,没有private修饰符,但它具有函数作用域。作用域的好处是内部函数可以访问它们的外部函数的参数和变量(除了this和argument
收藏 0 赞 0 分享

为JavaScript类型增加方法的实现代码(增加功能)

大家在js开发过程中有些功能已经满足不了我们的需求,或没有我们需要的功能,那么我们就可以自己扩展下,个性化js
收藏 0 赞 0 分享
查看更多