30天學習計劃 js忍者秘籍 第9章 忍者點金術:運行時代碼求值

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代碼字符串,用于在運行時進行求值操作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容