用函數(shù)模板,寫(xiě)一個(gè)簡(jiǎn)單高效的 JSON 查詢器的方法介紹
JSON可謂是JavaScript的亮點(diǎn),它能用優(yōu)雅簡(jiǎn)練的代碼實(shí)現(xiàn)Object和Array的初始化。同樣是基于文本的數(shù)據(jù)定義,它比符號(hào)分隔更有語(yǔ)義,比XML更簡(jiǎn)潔。因此越來(lái)越多的JS開(kāi)發(fā)中,使用它作為數(shù)據(jù)的傳輸和儲(chǔ)存。
JS數(shù)組內(nèi)置了不少有用的方法,方便我們對(duì)數(shù)據(jù)的查詢和篩選。例如我們有一堆數(shù)據(jù):
var heros = [
// 名============攻=====防=======力量====敏捷=====智力====
{name:'冰室女巫', DP:38, AP:1.3, Str:16, Agi:16, Int:21},
{name:'沉默術(shù)士', 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:'劇毒術(shù)士', DP:45, AP:3.1, Str:18, Agi:22, Int:15},
{name:'光之守衛(wèi)', DP:38, AP:1.1, Str:16, Agi:15, Int:22},
{name:'煉金術(shù)士', 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;
});
返回得到一個(gè)數(shù)組,包括符合條件的2個(gè)結(jié)果。
相比手工去寫(xiě)循環(huán)判斷,filter方法為我們提供了很大的方便。但它是基于函數(shù)回調(diào)的,所以每次使用必須寫(xiě)一個(gè)function,對(duì)于簡(jiǎn)單的查詢很是累贅,而且使用回調(diào)效率也大大降低。但這是也沒(méi)有辦法的,想簡(jiǎn)單必然要犧牲一定性能。 如果能使用比這更簡(jiǎn)單的語(yǔ)句,并且完全擁有代碼展開(kāi)時(shí)效率,該有是多么完美的事。
先來(lái)想象下,要是能將上面的代碼寫(xiě)成這樣,并且查詢速度和手寫(xiě)的遍歷判斷一樣:
var match = heros.select('@DP>40 AND @AP<4');
看上去有點(diǎn)像SQL,連語(yǔ)法都換了?這樣豈不是要寫(xiě)一個(gè)詞法分析,語(yǔ)義解釋等等等等一大堆的腳本引擎的功能了,沒(méi)個(gè)幾千上萬(wàn)行代碼都搞不定,而且效率肯定更糟了。。。如果想到那么復(fù)雜,那么你還沒(méi)深刻的理解腳本的精髓。但凡是腳本語(yǔ)言,都有運(yùn)行時(shí)動(dòng)態(tài)解釋代碼的接口,例如vbs的execute();js的eval(),new Function(),甚至創(chuàng)建一個(gè)<script>動(dòng)態(tài)寫(xiě)入代碼。
顯然,要是能將另一種語(yǔ)言,翻譯成js代碼,那么就可直接交給宿主來(lái)執(zhí)行了!
例如上面select中的字符,我們簡(jiǎn)單的將"@"替換成"e.", "AND"替換成"&&",于是就成了一個(gè)合法的js表達(dá)式,完全可以交給eval來(lái)執(zhí)行。
所以我們要做的,就是將原始語(yǔ)句翻譯成js語(yǔ)句來(lái)執(zhí)行。并且為了提高效率,將翻譯好的js表達(dá)式內(nèi)聯(lián)到一個(gè)上下文環(huán)境,生成一個(gè)可執(zhí)行的函數(shù)體,而不是每次遍歷中都依靠回調(diào)來(lái)判斷。
于是,函數(shù)模版就要派上用場(chǎng)了。
函數(shù)模版簡(jiǎn)介
在C++里面,有宏和類(lèi)模版這么個(gè)東西,可以讓一些計(jì)算在編譯階段就完成了,大幅提升了運(yùn)行時(shí)代碼的性能。腳本雖然沒(méi)有嚴(yán)格意義上的編譯,但在第一次執(zhí)行的時(shí)候會(huì)解析并充分的優(yōu)化,這是目前主流瀏覽器相互競(jìng)爭(zhēng)點(diǎn)。所以,我們要將重復(fù)eval的代碼,鑲嵌到事先提供的樣板函數(shù)里:一個(gè)準(zhǔn)備就緒,就差表達(dá)式計(jì)算的函數(shù):
/**
* 模版: tmplCount
* 功能: 統(tǒng)計(jì)arr數(shù)組中符合$express表達(dá)式的數(shù)量
*/
function tmplCount(arr) {
var count = 0;
for(var i = 0; i < arr.length; i++) {
var e = arr[i];
if($express) {
count++;
}
}
return count;
}
上面就是一個(gè)模板函數(shù),遍歷參數(shù)arr[]并統(tǒng)計(jì)符合$express的數(shù)量。除了if(...)內(nèi)的表達(dá)式外,其他都已經(jīng)準(zhǔn)備就緒了。字符$express也可以換成其他標(biāo)識(shí),只要不和函數(shù)內(nèi)其他字符沖突即可。
當(dāng)我們需要實(shí)例化時(shí),首先通過(guò)tmplCount.toString()將函數(shù)轉(zhuǎn)成字符串格式,然后將其中的$express替換成我們想要的表達(dá)式,最后eval這串字符,得到一個(gè)Function類(lèi)型的變量,一個(gè)模板函數(shù)的實(shí)例就產(chǎn)生了!
我們簡(jiǎn)單的演示下:
/**
* 函數(shù): createInstance
* 參數(shù): exp
* 一段js表達(dá)式字符串,用來(lái)替換tmplCount模板的$express
* 返回:
* 返回一個(gè)Function,模版tmplCount的實(shí)例
*/
function createInstance(exp)
{
// 替換模板內(nèi)的表達(dá)式
var code = tmplCount.toString()
.replace('$express', exp);
// 防止匿名函數(shù)直接eval報(bào)錯(cuò)
var fn = eval('0,' + code);
// 返回模板實(shí)例
return fn;
}
// 測(cè)試參數(shù)
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個(gè)
// demo2
var f2 = createInstance('e.name!="Jack" && e.age>=14');
alert(f2(student)); //2個(gè)
注意createInstance()的參數(shù)中,有個(gè)叫e的對(duì)象,它是在tmplCount模版中定義的,指代遍歷時(shí)的具體元素。返回的f1,f2就是tmplCount模板的兩個(gè)實(shí)例。最終調(diào)用的f1,f2函數(shù)中,已經(jīng)內(nèi)嵌了我們的表達(dá)式語(yǔ)句,就像我們事先寫(xiě)了兩個(gè)同樣功能的函數(shù)一樣,所以在遍歷的時(shí)候直接運(yùn)行表達(dá)式,而不用回調(diào)什么的,效率大幅提升。
其實(shí)說(shuō)白了,tmplCount的存在僅僅是為了提供這個(gè)函數(shù)的字符串而已,其本身從來(lái)不會(huì)被調(diào)用。事實(shí)上用字符串的形式定義也一樣,只不過(guò)用函數(shù)書(shū)寫(xiě)比較直觀,方便測(cè)試。
值得注意的是,如果腳本后期需要壓縮優(yōu)化,那么tmplCount模板絕對(duì)不能參與,否則對(duì)應(yīng)的"e."和"$express"都有可能發(fā)生變化。
JSON基本查詢功能
函數(shù)模板的用處和實(shí)現(xiàn)介紹完了,再來(lái)回頭看之前的JSON查詢語(yǔ)言。我們只需將類(lèi)似sql的語(yǔ)句,翻譯成js表達(dá)式,并且生成一個(gè)函數(shù)模板實(shí)例。對(duì)于相同的語(yǔ)句,我們可以進(jìn)行緩存,避免每次都翻譯。
首先我們實(shí)現(xiàn)查詢器的模板:
var __proto = Object.prototype;
//
// 模板: __tmpl
// 參數(shù): $C
// 說(shuō)明: 記錄并返回_list對(duì)象中匹配$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();
然后開(kāi)始寫(xiě)Object的select方法:
//
// select方法實(shí)現(xiàn)
//
var __cache = {};
__proto.select = function(exp) {
if(!exp)
return [];
var fn = __cache[exp];
try {
if(!fn) {
var code = __interpret(exp); //解釋表達(dá)式
code = __tmpl.replace('$C', code); //應(yīng)用到模版
fn = __cache[exp] = __compile(code); //實(shí)例化函數(shù)
}
return fn(this); //查詢當(dāng)前對(duì)象
}
catch(e) {
return [];
}
}
其中__cache表實(shí)現(xiàn)了查詢語(yǔ)句的緩存。對(duì)于重復(fù)的查詢,性能可以極大的提升。
function __compile() {
return eval('0,' + arguments[0]);
}
__compile之所以單獨(dú)寫(xiě)在一個(gè)空函數(shù)里,就是為了eval的時(shí)候有個(gè)盡可能干凈的上下文環(huán)境。
__interpret是整個(gè)系統(tǒng)的重中之重,負(fù)責(zé)將查詢語(yǔ)句翻譯成js語(yǔ)句。它的實(shí)現(xiàn)見(jiàn)智見(jiàn)仁,但盡可能簡(jiǎn)單,不要過(guò)度分析語(yǔ)法。
具體代碼查看:jsonselect.rar
出于演示,目前只實(shí)現(xiàn)部分基本功能。以后還可以加上 LIKE,BETWEEN,ORDER BY 等等常用的功能。
Demo
var heros = [
// 名============攻=====防=======力量====敏捷=====智力====
{name:'冰室女巫', DP:38, AP:1.3, Str:16, Agi:16, Int:21},
{name:'沉默術(shù)士', 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:'劇毒術(shù)士', DP:45, AP:3.1, Str:18, Agi:22, Int:15},
{name:'光之守衛(wèi)', DP:38, AP:1.1, Str:16, Agi:15, Int:22},
{name:'煉金術(shù)士', DP:49, AP:0.6, Str:25, Agi:11, Int:25}
//...
];
// 查詢:力量,敏捷 都超過(guò)20的
// 結(jié)果:娜迦海妖
var match = heros.select('@Str>20 AND @Agi>20');
// 查詢:“士”結(jié)尾的
// 結(jié)果:沉默術(shù)士,劇毒術(shù)士,煉金術(shù)士
var match = heros.select('right(@name,1)="士" ');
// 查詢:生命值 超過(guò)500的
// 結(jié)果:煉金術(shù)士
var match = heros.select('100 + @Str*19 > 500');
- js 把字符串當(dāng)函數(shù)執(zhí)行的方法
- javascript定義函數(shù)的方法
- javascript中使用replaceAll()函數(shù)實(shí)現(xiàn)字符替換的方法
- javascript的函數(shù)、創(chuàng)建對(duì)象、封裝、屬性和方法、繼承
- 解決火狐瀏覽器下JS setTimeout函數(shù)不兼容失效不執(zhí)行的方法
- js函數(shù)調(diào)用常用方法詳解
- Javascript 判斷是否存在函數(shù)的方法
- jquery與js函數(shù)沖突的兩種解決方法
- 多種方法實(shí)現(xiàn)當(dāng)jsp頁(yè)面完全加載完成后執(zhí)行一個(gè)js函數(shù)
- 一個(gè)非常全面的javascript URL解析函數(shù)和分段URL解析方法
- JavaScript函數(shù)的4種調(diào)用方法詳解
- javascript trim函數(shù)在IE下不能用的解決方法
- JavaScript的各種常見(jiàn)函數(shù)定義方法
- PHP使用json_encode函數(shù)時(shí)不轉(zhuǎn)義中文的解決方法
- js中匿名函數(shù)的創(chuàng)建與調(diào)用方法分析
- js使用split函數(shù)按照多個(gè)字符對(duì)字符串進(jìn)行分割的方法
- JS中獲取函數(shù)調(diào)用鏈所有參數(shù)的方法
- JavaScript子窗口調(diào)用父窗口變量和函數(shù)的方法
- 日常收集整理的JavaScript常用函數(shù)方法
相關(guān)文章
微信小程序開(kāi)發(fā)animation心跳動(dòng)畫(huà)效果
這篇文章主要為大家詳細(xì)介紹了微信小程序開(kāi)發(fā)animation心跳動(dòng)畫(huà)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08JavaScript中Array方法你該知道的正確打開(kāi)方法
string和array作為javascript內(nèi)置對(duì)象,其中許多方法無(wú)論是在開(kāi)發(fā)過(guò)程中,還是在面試的時(shí)候都有機(jī)會(huì)被面試官問(wèn)到,下面這篇文章主要給大家介紹了關(guān)于JavaScript中Array方法你該知道的正確打開(kāi)方法,需要的朋友可以參考下2018-09-09JS數(shù)據(jù)雙向綁定原理與用法實(shí)例分析
這篇文章主要介紹了JS數(shù)據(jù)雙向綁定原理與用法,結(jié)合實(shí)例形式分析了JavaScript數(shù)據(jù)雙向綁定相關(guān)原理、實(shí)現(xiàn)技巧與操作注意事項(xiàng),需要的朋友可以參考下2019-11-11TypeScript類(lèi)型聲明書(shū)寫(xiě)詳解
這篇文章主要介紹了TypeScript類(lèi)型聲明書(shū)寫(xiě)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08js函數(shù)setTimeout延遲執(zhí)行的簡(jiǎn)單介紹
設(shè)置指定的JS函數(shù)在指定的時(shí)間后執(zhí)行,可以利用setTimeout()函數(shù)。2013-07-07圖形編輯器中JS實(shí)現(xiàn)防誤操作之拖拽阻塞
這篇文章主要為大家介紹了圖形編輯器中JS實(shí)現(xiàn)防誤操作之拖拽阻塞實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03