首頁 > 易卦

總結陳詞:Eval邪在哪裡?

作者:由 崟嶽論軟體 發表于 易卦日期:2023-01-13

會在什麼位置建立對應的按鈕

總結陳詞:Eval邪在哪裡?

作者作為程式設計師,經常聽說各種的“經驗之談”。特別是“聳人聽聞”的萬惡之源/百萬美金的錯誤等駭人的題目。譬如:

GOTO是萬惡之源:GOTO導致邏輯與流程混亂

指標是萬惡之源:程式跑飛全怪它

Assert是萬惡之源:有了它,誰還考慮異常處理?

泛型程式設計、多重繼承、強制cast型別都是萬惡之源——C++大師如是說

Eval是萬惡之源:很多語言都有這個說法,當然本文以JavaSript環境下展開討論

經過一番打量,我們把Eval的一些問題列舉出來,並作中立客觀的討論:

問題一:安全隱患。安全是一個非常籠統的說法,既然Eval主要任務是從字串編譯執行,怎麼它就不安全呢。難道模組載入,動態函式生成(new Function)不是做同樣的工作?從原理上看都是一樣的。區別在於模組本身是以程式碼檔案的形式存在,而new Function多數情況也儘量保留有程式的形式。都是字串,模組(即程式碼檔案)首先在編輯器中就能夠看出一些程式的結構與來龍去脈。動態函式一般可以透過入參在生成程式碼中的位置,看出一些處理的方式的端倪。而eval編譯執行的字串可以是沒有任何線索的字串。有時候看似“平和的字串”可以顛覆認知。

總結陳詞:Eval邪在哪裡?

看似平和的字串

問題二:除錯困難。早期除錯被Eval的指令碼的確困難。現在主流JS執行環境,如Chrome與FireFox,Node。js都支援Eval指令碼(字串)的除錯。需要在Eval的字串末尾新增//# sourceURL(對指令碼是註釋,對偵錯程式是指示標誌)。偵錯程式會為指令碼建立一個虛擬出來的sourceUR檔案,其內容對應Eval的字串。

總結陳詞:Eval邪在哪裡?

Chrome內Eval指令碼除錯

如果指令碼是未格式化(美化)的程式碼,在除錯介面左下角有一個“Pretty print”按鈕用於美化程式碼,而除錯可以在美化之後的程式碼之上。

問題三:效率低下。因為每次eval都要經過“編譯,執行”的步驟,而一般的函式編譯一次就可以,JS引擎會處理好編譯的時機,但一般無論如何都是隻編譯一次。但我們可以這樣推理:如果只是執行一兩次eval操作,編譯的代價並不大。如果執行很多次的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間接呼叫的例子

那麼對三種呼叫的

eval執行規則

是什麼,顯然最重要的是給出eval是在什麼初始環境中執行的。

歸納如下:

直接呼叫(DirectCall):

將執行到eval呼叫時的上下文,作為eval執行指令碼的初始上下文。下面的例子中,如果要設定eval的this上下文,需要設定包含它的函式。如果直接透過eval。call方式設定,則變成了間接呼叫。

總結陳詞:Eval邪在哪裡?

直接呼叫:正確的設定this

總結陳詞:Eval邪在哪裡?

錯誤地設定this上下文

間接呼叫(IndirectCall):

將global上下文,作為eval執行指令碼的初始上下文。上例”錯誤地設定this上下文“中由於使用eval。call立即切換到了global上下文,並改變了global上下文中的值。然而,這只是ES5規範中的規則,

在ES3規範中對此沒有規定

。也就是說對於ES3規範的執行環境,有一些不確定的情況……

總結陳詞:Eval邪在哪裡?

node。js環境下的eval呼叫

隱含呼叫 (ImplicitCall):

將global上下文,作為eval執行指令碼的初始上下文。

總結:

經過上述分析,筆者認為Eval最大的難點在於區別出調用情況,並清楚執行的指令碼使用什麼上下文,對上下文造成何種的影響:可能混淆直接呼叫與間接呼叫,可能不清楚某個JS環境的global上下文的內容,可能想程式跨平臺(ES3/ES5, Node。js/瀏覽器)。

再加上沒有隻能透過Eval完成的任務,再加上以程式碼檔案/模組檔案的形式更有利於開發、除錯、維護。

筆者建議避免使用Eval函式,甚至避免動態函式生成(new Function)。

建議使用程式碼靜態檢查工具ESLINT,用規則no-eval避免直接呼叫與間接呼叫,用規則no-implied-eval避免隱含呼叫