會在什麼位置建立對應的按鈕
作者作為程式設計師,經常聽說各種的“經驗之談”。特別是“聳人聽聞”的萬惡之源/百萬美金的錯誤等駭人的題目。譬如:
GOTO是萬惡之源:GOTO導致邏輯與流程混亂
指標是萬惡之源:程式跑飛全怪它
Assert是萬惡之源:有了它,誰還考慮異常處理?
泛型程式設計、多重繼承、強制cast型別都是萬惡之源——C++大師如是說
Eval是萬惡之源:很多語言都有這個說法,當然本文以JavaSript環境下展開討論
經過一番打量,我們把Eval的一些問題列舉出來,並作中立客觀的討論:
問題一:安全隱患。安全是一個非常籠統的說法,既然Eval主要任務是從字串編譯執行,怎麼它就不安全呢。難道模組載入,動態函式生成(new Function)不是做同樣的工作?從原理上看都是一樣的。區別在於模組本身是以程式碼檔案的形式存在,而new Function多數情況也儘量保留有程式的形式。都是字串,模組(即程式碼檔案)首先在編輯器中就能夠看出一些程式的結構與來龍去脈。動態函式一般可以透過入參在生成程式碼中的位置,看出一些處理的方式的端倪。而eval編譯執行的字串可以是沒有任何線索的字串。有時候看似“平和的字串”可以顛覆認知。
看似平和的字串
問題二:除錯困難。早期除錯被Eval的指令碼的確困難。現在主流JS執行環境,如Chrome與FireFox,Node。js都支援Eval指令碼(字串)的除錯。需要在Eval的字串末尾新增//# sourceURL(對指令碼是註釋,對偵錯程式是指示標誌)。偵錯程式會為指令碼建立一個虛擬出來的sourceUR檔案,其內容對應Eval的字串。
Chrome內Eval指令碼除錯
如果指令碼是未格式化(美化)的程式碼,在除錯介面左下角有一個“Pretty print”按鈕用於美化程式碼,而除錯可以在美化之後的程式碼之上。
問題三:效率低下。因為每次eval都要經過“編譯,執行”的步驟,而一般的函式編譯一次就可以,JS引擎會處理好編譯的時機,但一般無論如何都是隻編譯一次。但我們可以這樣推理:如果只是執行一兩次eval操作,編譯的代價並不大。如果執行很多次的eval操作,說明這些eval操作有很大的共性部分,我們可以具有共性的部分提取出來,做eval返回一個函式型別的變數,只要變數不釋放,編譯好的內容就會一直保留。
提取共性部分進行最佳化
問題四:長得就很邪。如果知道英語“邪惡”怎麼拼寫,可以說長得像而已。
問題五:太複雜。初看很簡單,eval(字串)。但其中的隱含意義非常複雜,不容易掌握。
以下關於問題五,做一個小結式講解。為了更好的統一論述,也由於Javascript本身協議在一些概念上的混用,以下先明確本文使用的術語。
上下文:
指Execution context(執行上下文),包括函式的入參,自身的變數,this變數(以下稱this上下文),指向上層閉包/函式(又稱上層Scope)的指標。
this上下文:
又稱thisArgument, thisValue, receiver, ThisBinding等。在很多文件中稱為context(上下文),即函式中this變數對應的實際內容。考慮到this只是函式執行環境的一部分,筆者認為還是明確為“this上下文”更好。
Scope:
粗略地說,Scope指除了this之外的所有變數。
eval的呼叫實際分為三類:
直接呼叫(DirectCall):
語法上而言,被呼叫的函式是一個Reference
間接呼叫(IndirectCall):
語法上而言,被呼叫的函式是一個value值
隱含呼叫 (ImpliedCall):
沒有出現eval,但實際以eval實現的,指setTimeout與setInterval函式的第一引數是字串的情況
類似的例子表示式比較(expr1, expr2) === expr2中。左側以逗號相隔,每個expr分別求值。因為比較符號的存在,右側expr2也經過求值,最終兩側相等。比如(0, obj。someFunc)(),雖然obj。someFunc求值之後的指向位置沒錯,但也喪失了obj的資訊。傳入函式的“this上下文”並不是obj。而(obj。someFunc)()中obj。someFunc保持為Reference(不進行求值),函式呼叫時傳入obj作為this上下文。()運算子不要求“求值“。根據上述標準,可以大致歸納直接呼叫與間接呼叫的情形:
eval直接呼叫的例子
eval間接呼叫的例子
那麼對三種呼叫的
eval執行規則
是什麼,顯然最重要的是給出eval是在什麼初始環境中執行的。
歸納如下:
直接呼叫(DirectCall):
將執行到eval呼叫時的上下文,作為eval執行指令碼的初始上下文。下面的例子中,如果要設定eval的this上下文,需要設定包含它的函式。如果直接透過eval。call方式設定,則變成了間接呼叫。
直接呼叫:正確的設定this
錯誤地設定this上下文
間接呼叫(IndirectCall):
將global上下文,作為eval執行指令碼的初始上下文。上例”錯誤地設定this上下文“中由於使用eval。call立即切換到了global上下文,並改變了global上下文中的值。然而,這只是ES5規範中的規則,
在ES3規範中對此沒有規定
。也就是說對於ES3規範的執行環境,有一些不確定的情況……
node。js環境下的eval呼叫
隱含呼叫 (ImplicitCall):
將global上下文,作為eval執行指令碼的初始上下文。
總結:
經過上述分析,筆者認為Eval最大的難點在於區別出調用情況,並清楚執行的指令碼使用什麼上下文,對上下文造成何種的影響:可能混淆直接呼叫與間接呼叫,可能不清楚某個JS環境的global上下文的內容,可能想程式跨平臺(ES3/ES5, Node。js/瀏覽器)。
再加上沒有隻能透過Eval完成的任務,再加上以程式碼檔案/模組檔案的形式更有利於開發、除錯、維護。
筆者建議避免使用Eval函式,甚至避免動態函式生成(new Function)。
建議使用程式碼靜態檢查工具ESLINT,用規則no-eval避免直接呼叫與間接呼叫,用規則no-implied-eval避免隱含呼叫