SOURCE

function MathExp() {
    this.ops = ['+', '-', '*', '/', '%', '(', ')']
    this.priorityMap = {
        '*': 2,
        '/': 2,
        '%': 2,
        '+' : 1,
        '-': 1
    }
}

MathExp.prototype.compute = function (a, op, b) {
    if (op == '+') {
        return Number(a) + Number(b)
    } else if (op == '-') {
        return Number(a) - Number(b)
    } else if (op == '*') {
        return Number(a) * Number(b)
    } else if (op == '/') {
        return Number(a) / Number(b)
    } else if (op == '%') {
        return Number(a) % Number(b)
    }
}

MathExp.prototype.parseToken = function(exp) {
    const items = (exp || '').split('')
    const tokens = []
    let nums = []
    let placeholder = []

    const pushPlaceholder = () => {
        if (placeholder.length) {
            tokens.push({
                type: 'placeholder',
                value: placeholder.join('')
            })
            placeholder = []
        }
    }
    for (let item of items) {
        if (/[\d\.]/.test(item)) {
            if (placeholder.length > 0 && item === '.') {
                placeholder.push(item)
            } else {
                nums.push(item)
                pushPlaceholder()
            }
        } else {
            if (nums.length) {
                tokens.push({
                    type: 'number',
                    value: Number(nums.join(''))
                })
                nums = []
            }
            if (this.ops.includes(item)) {
                pushPlaceholder()
                tokens.push({
                    type: 'op',
                    value: item,
                    priority: this.priorityMap[item] || 0
                })
            } else if (!/\s/.test(item)) {
                placeholder.push(item)
            }
        }
    }
    if (nums.length > 0) {
        tokens.push({
            type: 'number',
            value: Number(nums.join(''))
        })
    }
    if (placeholder.length > 0) {
        pushPlaceholder()
    }
    return tokens
}

MathExp.prototype.updateExp = function(exp) {
    if (typeof exp === 'string') {
        this.tokens = this.parseToken(exp)
    } else {
        this.tokens = this.parseToken((exp || []).map(i => i.value).join(''))
    }
}

/**
 * 将普通的数学表达式 转换成 逆波兰(Reverse Polish Notation)形式
 * 这样做的目的是忽略掉运算符的优先级、括号的影响
 */
MathExp.prototype.dal2Rnp = function () {
    const opStack = []
    const output = []
    let i = 0, cnt = this.tokens.length
    let tmp = null
    while(i < cnt) {
        const token = this.tokens[i]
        if (token.type === 'number' || token.type === 'placeholder') {
            // 如果是数字 or 占位符 直接输出
            output.push(token)
        } else if (token.type === 'op') {
            if (['(', ')'].includes(token.value)) {
                if (token.value === '(') {
                    // 左括号 进入操作符栈
                    opStack.push(token)
                } else {
                    tmp = opStack.pop()
                    while(tmp.value !== '(' && opStack.length > 0) {
                        output.push(tmp)
                        tmp = opStack.pop()
                    }
                    if (tmp.value !== '(') {
                        throw 'error: unmatched ('
                    }
                }
            } else {
                while(opStack.length > 0 && token.priority <= opStack[opStack.length - 1].priority) {
                    tmp = opStack.pop()
                    output.push(tmp)
                }
                opStack.push(token)
            }
        }
        i++
    }
    if (opStack.length > 0) {
        while(opStack.length > 0) {
            tmp = opStack.pop()
            if (tmp.type === 'op' && ['(', ')'].includes(tmp.value)) {
                throw 'error: unmatched ('
            }
            output.push(tmp)
        }
    }
    return output
}



MathExp.prototype.evalRnp = function(queue, placeholderMap) {
    const output = []
    const tmpMap = placeholderMap || {}
    while(queue.length) {
        let tmp = queue.shift()
        if (tmp.type === 'number') {
            output.push(tmp.value)
        } else if (tmp.type === 'placeholder') {
            let pv = tmpMap[tmp.value]
            pv = !pv && pv !== 0 ? 1 : pv
            output.push(pv)
        } else if (tmp.type === 'op') {
            if (output.length < 2) {
                throw 'error: operator lacked'
            }
            let second = output.pop()
            let first = output.pop()
            const result = this.compute(first, tmp.value, second)
            output.push(result)
        }
    }
    if (output.length > 1) {
        throw 'invalid'
    }
    let val =  output[0]
    if (!val && val !== 0) {
        throw 'invalid'
    }
    return val
}

MathExp.prototype.eval = function(exp, placeholderMap) {
    this.updateExp(exp)
    let result = null
    try {
        result = this.evalRnp(this.dal2Rnp(), placeholderMap)
    } catch {
        result = null
    }
    //  result = this.evalRnp(this.dal2Rnp())
    return result
}

// '1+3.14/3*(金额+23.1)'
const formulas = [
    { type: 'number', value: 1 },
    { type: 'op', value: '+' },
    { type: 'number', value: 3 },
    { type: 'number', value: '.' },
    { type: 'number', value: 1 },
    { type: 'number', value: 4 },
    { type: 'op', value: '/', priority: 2 },
    { type: 'number', value: 3 },
    { type: 'op', value: '*' },
    { type: 'op', value: '('},
    { type: 'placeholder', value: 'price', name: '金额' },
    { type: 'op', value: '+' },
    { type: 'number', value: 2 },
    { type: 'number', value: 3 },
    { type: 'number', value: '.' },
    { type: 'number', value: 1 },
    { type: 'op', value: ')'},
]
const exp = new MathExp()
let result = exp.eval(formulas, { price: 100 })
console.log('根据数组:', result)

result = exp.eval('1+3.14/3*(金额+23.1)', { '金额': 100 })
console.log('根据字符串:', result)

console 命令行工具 X clear

                    
>
console