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查詢語言。我們只需將類似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語句。它的實現見智見仁,但盡可能簡單,不要過度分析語法。
具體代碼查看: http://files.cnblogs.com/index-html/jsonselect.js
出於演示,目前只實現部分基本功能。以后還可以加上 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');