最近徘徊在找工做和繼續留任的糾結之中,在朋友的慫恿下去參加了一次面試,最後一道題目是:javascript
寫一個函數,輸入一個字符串的運算式,返回計算以後的結果。例如這樣的: '1 + (5 - 2) * 3',計算出結果爲10html
最開始看到這個題目的時候,我腦中的第一反應就是eval
,真的太直接了。可是我就不明白爲何這居然是最後一道題目,我也不知道爲何還會考eval
的運用,所以當時也很猶豫要不要用eval
。由於eval
有一系列的問題:前端
eval
自己執行當時只是以爲可使用正則匹配運算符,而後使用遞歸計算,就只寫了個思路,回來以後就按照這個方式實現一下。這裏做爲本身的解決方式,測試用例設計的也不夠全面,若是各位有更好的方法,能夠拿出來分享。java
PS: 這是以後採用了各位大大們的指點寫出來的後綴表達式解決辦法,可是被博客園移除了首頁,暫時還不知道爲何。node
若是我拿個'1 + (5 - 2) * 3'這個式子我是怎麼想的:jquery
/%*
的優先級要大於+-
,先匹配計算出 3 * 3講白了就是有括號,先計算括號中的算是,而後進行結果替換以後再進行後面的運算,總體而言就是一系列的'遞歸 + 匹配'。面試
/** * myEval * @param string str 字符串 * @return 返回計算後的值 [description] */ function myEval(str) { // 若是包含括號,則先進括號中的計算 // 計算規則爲:先進行括號匹配拆開,單個計算以後再進行拼接 // 例如:((1 + 2) + 3 / (4 % 6)) * 6的計算順序是: // -> ((1 + 2) + 3 / (4 % 6)) * 6 // -> (1 + 2) + 3 / (4 % 6) // -> 1 + 2 // -> 3 + 3 / (4 % 6) // -> 4 % 6 // -> 3 + 3 / 4 // -> 3 / 4 // -> (3 + 0.75) * 6 // -> 3 + 0.75 // -> 3.75 * 6 // -> 22.5 if (exists(str, '(')) { const bracketStr = getMatchStr(str); const nextResult = myEval(bracketStr); const replaceStr = str.replace(`(${bracketStr})`, nextResult) // 若是子字符串中存在'3 + 3 / (4 % 6)' 這樣的式子,說明第一個括號中的內容計算完成了 // 這樣就能夠接着遞歸進行第二個括號中的算式計算 if (exists(replaceStr, '(')) { return myEval(replaceStr); } else { // 若是是相似於'1 + 2 / 3'的式子,則直接進行計算返回結果 return innerBracketCacl(replaceStr); } } else { return innerBracketCacl(str); } }
取一個叫作myEval
的函數,主要進行流程的控制,若是遇到的是括號中的內容,則先進行括號中的運算,不然,直接進行常規表達式計算。正則表達式
/** * 獲取匹配的字符串 * @param string str * @return string 返回的匹配結果 */ function getMatchStr(str) { // 匹配相似於這樣的式子: // '((1 + 2) / 3) * 4' -> ((1 + 2) / 3) // '1 * (2 + 3) / (5 - 6)' -> 2 + 3 const regexp = /\([^\)]+\)[^\(]+\)|\((.*?)\)/; const regexp2 = /\((.*)\)/; let matches = str.match(regexp); let bracketStr = matches[1] || matches[0]; if (exists(bracketStr, '(') && !exists(bracketStr, ')')) { // 相似於這樣的式子'((1 + 2) / (3 - 7)) * 4' // 那麼匹配出來的就是'(1 + 2' // 顯然不是我想要的結果,我只須要解掉第一層的括號就能夠按照以前的方式計算了 // 用第二個正則匹配的就是'(1 + 2) / (3 - 7)' // 我只須要按照以前的方式先計算這個式子就好 bracketStr = str.match(regexp2)[1]; } else if(bracketStr.indexOf('(') === 0) { bracketStr = bracketStr.slice(1, -1); } return bracketStr; }
獲取匹配字符子串,主要是進行規則匹配,分佈計算。app
/** * 計算表達式 * 例若有這樣的式子: '1 + 2 / 3' * 那麼會先計算'2 / 3' * @param string str * @return string 結果 */ function innerBracketCacl(str) { const matches = str.match(/[\/\*%]/g); let firstPriorityResult = str; if (matches) { firstPriorityResult = stepFirstPriority(str); } return stepSecondPriority(firstPriorityResult); }
簡單的運算式計算,即不包含括號的計算,先計算*/%
的運算符,而後計算+-
xss
/** * 第一優先級的運算 * 這裏的第一優先級爲'%/*' * @param string str * @return number 返回計算結果 */ function stepFirstPriority(str) { const matches = str.match(/[\/\*%]/g); if (!matches) { return str; } else { const newStr = caclPart('/%*', str); return stepFirstPriority(newStr); } } /** * 第二優先級的運算 * 這裏的第一優先級爲'+-' * @param string str * @return number 返回計算結果 */ function stepSecondPriority(str) { if (!isNaN(Number(str))) { return str; } else { const newStr = caclPart('+-', str); return stepSecondPriority(newStr); } }
這上面是運算優先級的計算方式,先乘除後加減,計算以後進行字符串替換,而後遞歸計算。
/** * 計算相似於 '1 + 2', '3 / 4'的子算式 * @param string shouldOprs 包含的運算符,例如('/%*', '+-') * @param string str 計算的子字符串,例如( 1 + 2 / 4 ) * @return string 返回計算後的子字符串,例如( 1 + 0.5 ) */ function caclPart(shouldOprs, str) { let newStr = ''; for (let i = 0; i < str.length; i++) { let s = str[i]; if (exists(shouldOprs, s)) { // 截取字符串的左側 // 例如字符串爲'3 + 3 / 4', 那麼左側就是'3 + 3 /',右側則是 / 4 // 目的是爲了接下來的匹配左右兩側的數字 let leftStr = str.slice(0, i + 1); let rightStr = str.slice(i); // 左側的正則爲/((\d\.)*\d+)\s*\+$/,其中最後一個'+'是動態匹配的字符串 // 右側的正則爲/\+\s*((\d\.)*\d+)/,其中最後一個'+'是動態匹配的字符串 const leftNum = new RegExp('\((\\d\\.)*\\d+\)\\s\*\\' + s + '$', 'g').exec(leftStr)[1]; const rightNum = new RegExp('\\' + s + '\\s\*\((\\d\\.)*\\d+\)').exec(rightStr)[1]; // 計算出值後進行字符串替換 // 好比'3 + 3 / 4' -> '3 + 0.75' // 單個計算完成以後跳出循環,以後繼續進行後面的操做 const result = cacl(leftNum, rightNum, s); newStr = str.replace(new RegExp('(\\d\\.)*\\d+\\s\*\\' + s + '\\s\*(\\d\\.)*\\d+'), result); break; } } return newStr; }
至此,這就是個人所有思路以及實現方式。
其中有一些正則表達式寫不出,想來正則學得仍是不夠,只能用一些取巧的辦法。測試用例也設計得不是太全面,可能會存在一些問題,可是就目前的測試來講,簡單的算是是能經過的。
性能問題上:由於頻繁的調用遞歸,導致複雜度大大增大,時間運行得也比原生eval
時間要長。如下是個人測試例子:
const str = '1 + 2'; const str2 = '1 + 2 - 3'; const str3 = '1 + 2 + 3 / 4'; const str4 = '1 + 2 + 3 / 4 % 5'; const str5 = '1 + 2 * (3 + 4) + 5'; const str6 = '(1 + 2) * (3 + 4) + 5'; const str7 = '((1 + 2) + 3 / (4 % 6)) * 6'; console.time('myEval'); console.log('myEval: ', myEval(str)); console.log('myEval: ', myEval(str2)); console.log('myEval: ', myEval(str3)); console.log('myEval: ', myEval(str4)); console.log('myEval: ', myEval(str5)); console.log('myEval: ', myEval(str6)); console.log('myEval: ', myEval(str7)); console.timeEnd('myEval') console.time('eval'); console.log('eval: ', eval(str)); console.log('eval: ', eval(str2)); console.log('eval: ', eval(str3)); console.log('eval: ', eval(str4)); console.log('eval: ', eval(str5)); console.log('eval: ', eval(str6)); console.log('eval: ', eval(str7)); console.timeEnd('eval')
關於js
實現eval
的方式:
//計算表達式的值 function evil(fn) { var Fn = Function; //一個變量指向Function,防止有些前端編譯工具報錯 return new Fn('return ' + fn)(); } // jquery2.0.3實現方式: // Evaluates a script in a global context globalEval: function( code ) { var script, indirect = eval; code = jQuery.trim( code ); if ( code ) { // If the code includes a valid, prologue position // strict mode pragma, execute code by injecting a // script tag into the document. if ( code.indexOf("use strict") === 1 ) { script = document.createElement("script"); script.text = code; document.head.appendChild( script ).parentNode.removeChild( script ); } else { // Otherwise, avoid the DOM node creation, insertion // and removal by using an indirect global eval indirect( code ); } } }
參考資料: