9.1.1 用eval()方法進行求值
eval()方法可能是在運行時進行代碼求值的最常用方式了。作為定義在全局作用域內的eval()方法,該方法將在當前上下文內,執行所傳入字符串形式的代碼。執行返回結果則是最后一個表達式的執行結果。
1)基本功能
該方法將執行傳入代碼的字符串,在調用eval()方法的作用域內進行代碼求值。
示例9.1 eval()方法的基本測試
test?suite
#results?.pass{color:green;}
#results?.fail{color:red;}
function?assert(value,desc){
var?li?=?document.createElement('li');
li.className?=?value???'pass'?:?'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
assert(eval('5+5')?===?10,'5?and?5?is?10');
assert(eval('var?ninja?=?5;')?===?undefined,'no?value?was?returned.');
assert(ninja?===?5,'The?variable?ninja?was?created');
(function(){
eval('var?ninja?=?6;');
assert(ninja?===?6,'evaluated?within?the?current?scope.');
})()
assert(window.ninja?===?5,'this?global?scope?was?unaffected.');
assert(ninja?===?5,'the?global?scope?was?unaffected.');
2)求值結果
eval()方法將返回傳入字符串中最后一個表達式的執行結果。
eval('3+4;5+6') 結果將返回11
任何不是簡單變量、原始值、賦值語句的內容都需要在外面包裝一個括號,以便返回正確的結果。
var o = eval('({ninja:1})')
示例9.2 測試eval()的返回結果
test?suite
#results?.pass{color:green;}
#results?.fail{color:red;}
function?assert(value,desc){
var?li?=?document.createElement('li');
li.className?=?value???'pass'?:?'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
var?ninja?=?eval('({name:"ninja"})');
assert(ninja?!=?undefined,'the?ninja?was?created');
assert(ninja.name?===?'ninja','and?with?the?expected?property');
var?fn?=?eval('(function(){return?"ninja";})');
assert(typeof?fn?===?'function','the?function?as?created');
assert(fn()?===?'ninja','and?returns?expected?value');
var?ninja2?=?eval('{name:"ninja"}');
assert(ninja2?!=?undefined,'ninja2?was?created.');
assert(ninja2.name?===?'ninja','and?with?the?expected?property');
最后一個測試失敗了,因為對象沒有按照預期進行創建。
就像我們用普通方式在特定作用域內創建函數一樣,eval()創建的函數會繼承該作用域的閉包——局部作用域內執行eval()的衍生結果。
9.1.2 用函數構造器進行求值
js中所有的函數都是Function的實例,可以通過像function name(){}這樣的語法創建命名函數,或者省略名稱創建匿名函數。
也可以直接使用Function構造器來實例化函數。
var add = new Function('a','b','return a+b');
assert(add(3,4)===7,'Function created and working!);
Function構造器可變參數列表的最后一個參數,始終是要創建函數的函數體內容。前面的參數則表示函數的形參名稱。
上邊代碼等價于: var add = function(a,b){return a+b}
雖然這些代碼在功能上是等同的,但采用Function構造器方式有一個明顯的區別,函數體由運行時的字符串所提供。
另外一個極其重要的實現區別是,使用Function構造器創建函數的時候,不會創建閉包。在不想承擔任何不相關的閉包的開銷時,這可能是一件好事。
9.1.3 用定時器進行求值
通過定時器可以讓代碼字符串進行求值,而且是異步的。
我們通常給定時器傳遞一個內聯函數或函數引用。這是setTimeout()和setInterval()方法推薦使用的方式,但是這些方法也可以接受字符串的傳入,從而在定時器觸發的時候進行求值。
var tick = window.setTimeout('alert("hi")',100)
這種方式使用情況不多,除非要求值的代碼必須是運行時字符串。
9.1.4全局作用域內的求值操作
示例9.3 在全局作用域內求值代碼
test?suite
#results?.pass{color:green;}
#results?.fail{color:red;}
function?assert(value,desc){
var?li?=?document.createElement('li');
li.className?=?value???'pass'?:?'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function?globalEval(data){
data?=?data.replace(/^\s*|\s*$/g,'');
if(data){
var?head?=?document.getElementsByTagName('head')[0]||document.documentElement,
script?=?document.createElement('script');
script.type?=?'text/javascript';
script.text?=?data;
head.appendChild(script);
head.removeChild(script);
}
}
window.onload?=?function(){
(function(){
globalEval('var?test=5;');
})()
assert(test===5,'The?code?was?evaluated?globally.')
}
9.1.5 安全的代碼求值
一個命名為Caja的谷歌項目,嘗試創建一個js翻譯器,以便將js轉換成一種更安全且免受惡意攻擊的形式。
http://code.google.com/p/google-caja/
9.2 函數反編譯
示例9.4 將函數反編譯成字符串
function?test(a){return?a+a;}
assert(test.toString()==='function?test(a){return?a+a;}','function?decompiled')
toString()的返回值包含原始聲明的所有空格,包括行結束符。請注意,在反編譯函數的時候,需要考慮空格和函數體的格式。
反編譯行為有很多潛在的用途,尤其是在宏指令和代碼重寫的時候。在Prototype js庫中,有一個比較有趣的應用是,將函數進行反編譯從而讀取該函數的參數,然后將這些參數名稱保存到一個數組中。通常用于確定函數想得到什么樣的參數值。
示例9.5 查找函數參數名稱的函數
test?suite
#results?.pass{color:green;}
#results?.fail{color:red;}
function?assert(value,desc){
var?li?=?document.createElement('li');
li.className?=?value???'pass'?:?'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function?argumentNames(fn){
var?found?=?/^[\s\(]*function[^(]*\(\s*([^)]*?)\s*\)/.exec(fn.toString());
return?found?&&?found[1]???found[1].split(/,\s*/)?:?[];
}
assert(argumentNames(function(){}).length?===?0,'works?on?zero-arg?functions.')
assert(argumentNames(function(x){})[0]?===?'x','single?argument?working.')
var?results?=?argumentNames(function(a,b,c,d,e){});
assert(results[0]?==?'a'?&&?results[1]?==?'b'?&&?results[2]?==?'c'?&&?results[3]?==?'d'?&&?results[4]?==?'e','multiple?arguments?working.')
該函數反編譯了傳入的函數,并使用正則表達式,將這些參數從逗號分隔的參數列表中抽取出來。
9.3 代碼求值實戰
9.3.1 JSON轉化
示例9.6 將JSON字符串轉化成js對象
var?json?=?'{"name":"ninja"}';
var?object?=?eval('('+json+')');
assert(object.name?===?'ninja','my?name?is?ninja!');
使用eval()做JSON解析時需要注意的主要是:通常,JSON數據來自于遠程服務器,盲目執行遠程服務器上不可信代碼,基本是不可取的。
最受歡迎的JSON轉換器腳本是由JSON標記的創造者所編寫的,在該轉換器中,他做了一些初步的JSON字符串解析,以防止任何惡意信息通過。代碼地址:https://github.com/douglascrockford/JSON-js
他寫的函數在實際求值之前,執行一些重要的預處理操作。
.防范一些可能在某些瀏覽器上引起問題的Unicode字符。
.防范惡意顯示的非JSON內容,包括賦值運算符和new操作符。
.確保只包含了符合JSON規范的字符。
9.3.2 導入有命名空間的代碼
對于將命名空間導入到當前上下文,base2庫提供了一個非常有趣的解決方案。因為沒有辦法將該問題進行自動化操作,因此我們可以利用運行時求值讓該實現變得簡單。
每當一個新類或模塊添加到base2包的時候,構造可執行代碼的字符串,對其進行求值,可以將產生的函數引入到當前上下文中,示例如下,假設已經加載了base2。
示例9.7 測試base2的命名空間導入是如何工作的。
base2.namespace?==??????????????????????????????????????????//#1
"var?Base=base2.Base;var?Package=base2.Package;"?+
"var?Abstract=base2.Abstract;var?Module=base2.Module;"?+
"var?Enumerable=base2.Enumerable;var?Map=base2.Map;"?+
"var?Collection=base2.Collection;var?RegGrp=base2.RegGrp;"?+
"var?Undefined=base2.Undefined;var?Null=base2.Null;"?+
"var?This=base2.This;var?True=base2.True;var?False=base2.False;"?+
"var?assignID=base2.assignID;var?detect=base2.detect;"?+
"var?global=base2.global;var?lang=base2.lang;"?+
"var?JavaScript=base2.JavaScript;var?JST=base2.JST;"?+
"var?JSON=base2.JSON;var?IO=base2.IO;var?MiniWeb=base2.MiniWeb;"?+
"var?DOM=base2.DOM;var?JSB=base2.JSB;var?code=base2.code;"?+
"var?doc=base2.doc;";
assert(typeof?This?===?"undefined",??????????????????????????//#2
"The?This?object?doesn't?exist."?);
eval(base2.namespace);???????????????????????????????????????//#3
assert(typeof?This?===?"function",???????????????????????????//#4
"And?now?the?namespace?is?imported."?);
assert(typeof?Collection?===?"function",
"Verifying?the?namespace?import."?);
這是一個用于解決復雜問題的非常巧妙的方法。
9.3.3 JS壓縮和混淆
最好是將代碼寫得越清晰越好,然后再進行壓縮傳輸。
壓縮js代碼的工具 packerhttp://dean.edwards.name/packer/使用eval()進行大規模的重寫和解壓
下載和求值之間的組合對頁面的性能才是最重要的。
加載時間 = 下載時間+求值時間
使用簡單壓縮性能是最好的,如果要用代碼混淆,可以使用packer
9.3.4 動態重寫代碼
由于我們可以使用函數的toString()方法反編譯現有的js函數,可以從現有函數中提取并加工原有函數的內容,從而創建一個 新函數。
單元測試庫Screw.Unit(https://github.com/nkallen/screw-unit),就是一個這樣的案例。
Screw.Unit使用庫中提供的函數,將現有測試函數中的內容進行了動態重寫。
describe('Matchers',function(){
it('invokes?the?provided?matcher?on?a?call?to?expect',function(){
expect(true).to(equal,true);
expect(true).to_not(equal,false);
})
})
describe(),it()以及expect(),這些方法在全局作用域內都不存在。Screw.Unit重寫了這段代碼,使用多個width(){}語句,將函數內部的內容注入到需要執行的函數中。
var?contents?=?fn.toString().match(/^[^{]*{((.*\n*)*)}/m)[1];
var?fn?=?new?Function('matchers','specifications','with(specifications){width(matchers){'+contents+'}}')
fn.call(this.Screw.Matchers,Screw.specifications);
這是一個讓測試開發人員在無需將變量引入到全局作用域的情況下,利用代碼求值就可以提供簡潔用戶體驗功能的場景。
9.3.5 面向切面的腳本標簽
AOP,面向方面編程。
AOP技術可以在運行時將代碼進行注入并執行一些“橫切”代碼,如日志記錄、緩存、安全性檢查等。AOP引擎將在運行時添加日志代碼,而不是在原有代碼中添加大量的日志語句,以便讓開發人員在開發期間不用關注這些事情。
定義自定義腳本類型是非常簡單的,因為瀏覽器會忽略任何無法識別的腳本類型。通過使用一個不標準的類型值,我們可以強制瀏覽器完全忽視一個腳本塊。
...
注意,我們使用統一約定的“x”表示自定義類型。我們打算用這樣的塊來包含正常的js代碼,以便在頁面加載時進行執行,而不是通常的內聯執行。
示例9.8 創建一個在頁面加載后才執行的腳本標簽類型
test?suite
#results?.pass{color:green;}
#results?.fail{color:red;}
function?assert(value,desc){
var?li?=?document.createElement('li');
li.className?=?value???'pass'?:?'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
function?globalEval(data){
data?=?data.replace(/^\s*|\s*$/g,'');
if(data){
var?head?=?document.getElementsByTagName('head')[0]||document.documentElement,
script?=?document.createElement('script');
script.type?=?'text/javascript';
script.text?=?data;
head.appendChild(script);
head.removeChild(script);
}
}
window.onload?=?function(){
var?scripts?=?document.getElementsByTagName('script');
for(var?i=0;?i
if(scripts[i].type?==?'x/onload'){
globalEval(scripts[i].innerHTML)
}
}
}
assert(true,'Executed?on?page?load')
在本例中,我們提供一個瀏覽器忽略執行的自定義腳本塊。在頁面的onload處理程序中,查詢所有的腳本塊,再篩選自定義類型的腳本塊,最后用本章前面開發的globalEval()函數,在全局作用域內對腳本塊的內容進行求值。
這種技術有更復雜更有意義的用途。例如,將自定義腳本塊和jQuery.tmpl()方法一起使用,用于提供運行時模板。利用它可以在用戶界面上執行腳本,或者在準備操作DOM的時候,甚至是相鄰元素上執行腳本。
9.3.6 元語言和領域特定語言
關于運行時代碼求值的一個最重要示例,可以在構建于js之上的其他編程語言實現中看到:元語言。可以將其動態轉換成js源代碼并求值。通常,這種定制語言非常特定于開發人員的業務需求,并且已經創建了領域特定語言(DSL)這樣的名字。
Processing.js
Processing.js是Processing(http://processing.org/)可視化語言的一部分,該可視化語言通常使用java實現。js的實現運行在HTML5的Canvas元素上,由John Resig創建。
這種實現是一種完整的編程語言,可以用來操作繪圖區域的視覺顯示。
通過使用Processing.js語言,我們獲得一些使用js時所沒有的直接好處。
.從Processing高級語言特性中獲益(如類和繼承)
.獲取Processing的簡單但強大的繪圖API
.可以使用Processing現有的文檔和示例。
所以這些高級處理代碼,都可以通過js語言的代碼求值功能來實現。
Objective-J
是Objective-C編程語言的js實現,被用于280Slides產品。
Objective-J解析程序,是由js編寫的,并可以在運行階段轉換Objective-J代碼,它們使用輕量級表達式進行匹配并處理Objective-C語法代碼,而不會干擾現有的js代碼。其處理結果是一個js代碼字符串,用于在運行時進行求值操作。