日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > HTML >内容正文

HTML

[前端漫谈] 做一个四则计算器

發(fā)布時間:2025/3/8 HTML 17 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [前端漫谈] 做一个四则计算器 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

0x000 概述

近期重新開始學(xué)習(xí)計(jì)算機(jī)基礎(chǔ)方面的東西,比如計(jì)算機(jī)組成原理、網(wǎng)絡(luò)原理、編譯原理之類的東西,目前正好在學(xué)習(xí)編譯原理,開始對這一塊的東西感興趣,但是理論的學(xué)習(xí)有點(diǎn)枯燥無味,決定換種方式,那就是先實(shí)踐、遇到問題嘗試解決,用實(shí)踐推動理論。原本打算寫個中文JS解析的,但是好像有點(diǎn)難,先就找個簡單的來做吧,那就是解析一下四則運(yùn)算,就有了這個項(xiàng)目,聲明:這是一個很簡單的項(xiàng)目,這是一個很簡單的項(xiàng)目,這是一個很簡單的項(xiàng)目。其中用到的詞法分析、語法分析、自動機(jī)都是用簡單的方式實(shí)現(xiàn),畢竟比較菜。

0x001 效果

  • 源碼地址:github

  • 實(shí)現(xiàn)功能:

    • 任意順序的四則+-*/正整數(shù)運(yùn)算
    • 支持()
    • 前端后端通用
    • 提供直接計(jì)算函數(shù)
    • 提供四則運(yùn)算表達(dá)式轉(zhuǎn)逆波蘭AST函數(shù)
    • 提供語法分析函數(shù)(暫時只支持上下兩個字符判定)
  • 效果演示:

0x002 實(shí)現(xiàn)

既然說很簡單,那不管用到的理論和實(shí)現(xiàn)的方式都一定要都很簡單,實(shí)現(xiàn)這個效果一共需要克服三個問題:

  • 如何實(shí)現(xiàn)優(yōu)先級計(jì)算,比如*/()的優(yōu)先級大于+-。
  • 如何分割字符串,比如如何識別數(shù)字、符號和錯誤字符,也就是詞素化。
  • 如何實(shí)現(xiàn)語法檢測,也就是讓表達(dá)式的規(guī)則滿足要求,比如+后面比如跟隨數(shù)字或者((這里將-當(dāng)作操作,而不是符號)。
  • 0x003 解決問題1: 如何實(shí)現(xiàn)優(yōu)先級運(yùn)算

    1. 暫時忽略優(yōu)先級

    如果沒有優(yōu)先級問題,那實(shí)現(xiàn)一個計(jì)算十分的簡單,比如下面的代碼可以實(shí)現(xiàn)一個簡單的加減或者乘除計(jì)算(10以內(nèi),超過一位數(shù)會遇到問題2,這里先簡單一點(diǎn),避過問題2):

    let calc = (input) => {let calMap = {'+': (num1, num2) => num1 + num2,'-': (num1, num2) => num1 - num2,'*': (num1, num2) => num1 * num2,'/': (num1, num2) => num1 / num2,}input = [...input].reverse()while (input.length >= 2) {let num1 = +input.pop()let op = input.pop()let num2 = +input.pop()input.push(calMap[op](num1, num2))}return input[0]}expect(calc('1+2+3+4+5-1')).toEqual(14)expect(calc('1*2*3/3')).toEqual(2) 復(fù)制代碼

    算法步驟:

    • 將輸入打散成一個棧,因?yàn)槭?0以內(nèi)的,所以每個數(shù)只有一位:input = [...input].reverse() 復(fù)制代碼
    • 每次取出三位,如果是正確的輸入,則取出的三位,第一位是數(shù)字,第二位是操作符,第三位是數(shù)字:let num1 = +input.pop() let op = input.pop() let num2 = +input.pop() 復(fù)制代碼
    • 根據(jù)操作符做運(yùn)算后將結(jié)果推回棧中,又形成了這么一個流程,一直到最后棧中只剩下一個數(shù),或者說每次都要取出3個數(shù),所以如果棧深度<=2,那就是最后的結(jié)果了:while (input.length >= 2) {// ......input.push(calMap[op](num1, num2)) } 復(fù)制代碼

    動畫演示:

    2. 考慮優(yōu)先級

    但是現(xiàn)在需要考慮優(yōu)先級,比如*/的優(yōu)先級大于+-,()的運(yùn)算符最高,那如何解決呢,其實(shí)都已經(jīng)有解決方案了,我用的是后綴表達(dá)式,也叫逆波蘭式

    • 后綴表達(dá)式: 所謂的后綴表達(dá)式,就是將操作符放在表達(dá)式的最后邊,比如1+1表示成11+。
    • 中綴表達(dá)式: 所謂的中綴表達(dá)式,其實(shí)就是我們平常使用的寫法了,這里不做深入。
    • 前綴表達(dá)式 所謂的后綴表達(dá)式,就是將操作符放在表達(dá)式的最前邊,比如1+1表示成+11,這里不做深入

    逆波蘭式可以參考下列文章

    • Wiki-逆波蘭表示法
    • 知乎-什么是逆波蘭表達(dá)式

    3. 逆波蘭式解決優(yōu)先級問題

    在逆波蘭式子中,1+1*2可以轉(zhuǎn)化為112*+ 代碼演示:

    let calc = (input) => {let calMap = {'+': (num1, num2) => num1 + num2,'-': (num1, num2) => num1 - num2,'*': (num1, num2) => num1 * num2,'/': (num1, num2) => num1 / num2,}input = [...input].reverse()let resultStack = []while (input.length) {let token = input.pop()if (/[0-9]/.test(token)) {resultStack.push(token)continue}if (/[+\-*/]/.test(token)) {let num1 = +resultStack.pop()let num2 = +resultStack.pop()resultStack.push(calMap[token](num1, num2))continue}}return resultStack[0] } expect(calc('123*+')).toEqual(7) 復(fù)制代碼

    轉(zhuǎn)化之后計(jì)算步驟如下:

  • 初始化一個棧 let resultStack = [] 復(fù)制代碼
  • 每次從表達(dá)式中取出一位let token = input.pop() 復(fù)制代碼
  • 如果是數(shù)字,則推入棧中if (/[0-9]/.test(token)) {resultStack.push(token)continue } 復(fù)制代碼
  • 如果是操作符,則從棧中取出兩個數(shù),做相應(yīng)的運(yùn)算,再將結(jié)果推入棧中if (/[+\-*/]/.test(token)) {let num1 = +resultStack.pop()let num2 = +resultStack.pop()resultStack.push(calMap[token](num1, num2))continue } 復(fù)制代碼
  • 如果表達(dá)式不為空,進(jìn)入步驟2,如果表達(dá)式空了,棧中的數(shù)就是最后的結(jié)果,計(jì)算完成while (input.length) {// ... } return resultStack[0] 復(fù)制代碼
  • 動畫演示:

    轉(zhuǎn)化成逆波蘭式之后有兩個優(yōu)點(diǎn):

    • 不關(guān)心運(yùn)算符優(yōu)先級
    • 去除括號,比如(1+2)*(3+4),可以轉(zhuǎn)化為12+34+*,按照逆波蘭式運(yùn)算方法即可完成運(yùn)算

    4. 中綴轉(zhuǎn)后綴

    這是問題1的最后一個小問題了,這個問題的實(shí)現(xiàn)過程如下:

    let parse = (input) => {input = [...input].reverse()let resultStack = [], opStack = []while (input.length) {let token = input.pop()if (/[0-9]/.test(token)) {resultStack.push(token)continue}if (/[+\-*/]/.test(token)) {opStack.push(token)continue}}return [...resultStack, ...opStack.reverse()].join('')}expect(parse(`1+2-3+4-5`)).toEqual('12+3-4+5-') 復(fù)制代碼

    準(zhǔn)備兩個棧,一個棧存放結(jié)果,一個棧存放操作符,最后將兩個棧拼接起來上面的實(shí)現(xiàn)可以將1+2-3+4-5轉(zhuǎn)化為12+3-4+5-,但是如果涉及到優(yōu)先級,就無能為力了,例如

    expect(parse(`1+2*3`)).toEqual('123*+') 復(fù)制代碼

    1+2*3的轉(zhuǎn)化結(jié)果應(yīng)該是123*+,但其實(shí)轉(zhuǎn)化的結(jié)果卻是123+*,*/的優(yōu)先級高于+,所以,應(yīng)該做如下修改

    let parse = (input) => {input = [...input].reverse()let resultStack = [], opStack = []while (input.length) {let token = input.pop()if (/[0-9]/.test(token)) {resultStack.push(token)continue} // if (/[+\-*/]/.test(token)) { // opStack.push(token) // continue // }if (/[*/]/.test(token)) {while (opStack.length) {let preOp = opStack.pop()if (/[+\-]/.test(preOp)) {opStack.push(preOp)opStack.push(token)token = nullbreak} else {resultStack.push(preOp)continue}}token && opStack.push(token)continue}if (/[+\-]/.test(token)) {while (opStack.length) {resultStack.push(opStack.pop())}opStack.push(token)continue}}return [...resultStack, ...opStack.reverse()].join('')}expect(parse(`1+2`)).toEqual('12+')expect(parse(`1+2*3`)).toEqual('123*+') 復(fù)制代碼
  • 當(dāng)操作符為*/的時候,取出棧頂元素,判斷棧中的元素的優(yōu)先級低是否低于*/,如果是就直接將操作符推入opStack,然后退出,否則一直將棧中取出的元素推入resultStack。
  • if (/[+\-]/.test(preOp)) {opStack.push(preOp)// 這里用了棧來做判斷,所以判斷完還得還回去...opStack.push(token)token = nullbreak }else {resultStack.push(preOp)continue } 復(fù)制代碼
  • 還要注意棧空掉的情況,需要將操作符直接入棧。
  • token && opStack.push(token)continue 復(fù)制代碼
  • 當(dāng)操作符為+-的時候,因?yàn)橐呀?jīng)是最低的優(yōu)先級了,所以直接將所有的操作符出棧就行了
  • if (/[+\-]/.test(token)) {while (opStack.length) {resultStack.push(opStack.pop())}opStack.push(token)continue } 復(fù)制代碼

    到這里已經(jīng)解決了+-*/的優(yōu)先級問題,只剩下()的優(yōu)先級問題了,他的優(yōu)先級是最高的,所以這里做如下修改即可:

    if (/[+\-]/.test(token)) {while (opStack.length) {let op=opStack.pop()if (/\(/.test(op)){opStack.push(op)break}resultStack.push(op)}opStack.push(token)continue } if (/\(/.test(token)) {opStack.push(token)continue } if (/\)/.test(token)) {let preOp = opStack.pop()while (preOp !== '('&&opStack.length) {resultStack.push(preOp)preOp = opStack.pop()}continue } 復(fù)制代碼
  • 當(dāng)操作符是+-的時候,不再無腦彈出,如果是(就不彈出了
  • while (opStack.length) {let op=opStack.pop()if (/\(/.test(op)){opStack.push(op)break}resultStack.push(op)}opStack.push(token) 復(fù)制代碼
  • 當(dāng)操作符是(的時候,就推入opStack
  • if (/\(/.test(token)) {opStack.push(token)continue } 復(fù)制代碼
  • 當(dāng)操作符是)的時候,就持續(xù)彈出opStack到resultStack,直到遇到(,(不推入resultStack
  • if (/\)/.test(token)) {let preOp = opStack.pop()while (preOp !== '('&&opStack.length) {resultStack.push(preOp)preOp = opStack.pop()}continue } 復(fù)制代碼

    動畫示例:

    如此,就完成了中綴轉(zhuǎn)后綴了,那么整個問題1就已經(jīng)被解決了,通過calc(parse(input))就能完成中綴=>后綴=>計(jì)算的整個流程了。

    0x004 解決問題2:分割字符串

    雖然上面已經(jīng)解決了中綴=>后綴=>計(jì)算的大問題,但是最基礎(chǔ)的問題還沒解決,那就是輸入問題,在上面問題1的解決過程中,輸入不過是簡單的切割,而且還局限在10以內(nèi)。而接下來,要解決的就是這個輸入的問題,如何分割輸入,達(dá)到要求?

    • 解決方式1:正則,雖然正則可以做到如下,做個簡單的demo還是可以的,但是對于之后的語法檢測之類的東西不太有利,所以不太好,我放棄了這種方法(1+22)*(333+4444)`.match(/([0-9]+)|([+\-*/])|(\()|(\))/g) // 輸出 // (11)["(", "1", "+", "22", ")", "*", "(", "333", "+", "4444", ")"] 復(fù)制代碼
    • 解決方法2:逐個字符分析,其大概的流程是while(input.length){let token = input.pop()if(/[0-9]/.test(token)) // 進(jìn)入數(shù)字分析if(/[+\-*/\(\)]/.test(token))// 進(jìn)入符號分析 } 復(fù)制代碼

    接下來試用解決方案2來解決這個問題:

    1 定義節(jié)點(diǎn)結(jié)構(gòu)

    當(dāng)我們分割的時候,并不單純保存值,而是將每個節(jié)點(diǎn)保存成一個相似的結(jié)構(gòu),這個結(jié)構(gòu)可以使用對象表示:

    {type:'',value:'' } 復(fù)制代碼

    其中,type是節(jié)點(diǎn)類型,可以將四則運(yùn)算中所有可能出現(xiàn)的類型歸納出來,我的歸納如下:

    TYPE_NUMBER: 'TYPE_NUMBER', // 數(shù)字TYPE_LEFT_BRACKET: 'TYPE_LEFT_BRACKET', // (TYPE_RIGHT_BRACKET: 'TYPE_RIGHT_BRACKET', // )TYPE_OPERATION_ADD: 'TYPE_OPERATION_ADD', // +TYPE_OPERATION_SUB: 'TYPE_OPERATION_SUB', // -TYPE_OPERATION_MUL: 'TYPE_OPERATION_MUL', // *TYPE_OPERATION_DIV: 'TYPE_OPERATION_DIV', // / 復(fù)制代碼

    value則是對應(yīng)的真實(shí)值,比如123、+、-、*、/。

    2 數(shù)字處理

    如果是數(shù)字,則繼續(xù)往下讀,直到不是數(shù)字為止,將這過程中所有的讀取結(jié)果放到value中,最后入隊(duì)。

    if (token.match(/[0-9]/)) {let next = tokens.pop()while (next !== undefined) {if (!next.match(/[0-9]/)) breaktoken += nextnext = tokens.pop()}result.push({type: type.TYPE_NUMBER,value: +token})token = next } 復(fù)制代碼

    3 符號處理

    先定義一個符號和類型對照表,如果不在表中,說明是異常輸入,拋出異常,如果取到了,說明是正常輸入,入隊(duì)即可。

    const opMap = {'(': type.TYPE_LEFT_BRACKET,')': type.TYPE_RIGHT_BRACKET,'+': type.TYPE_OPERATION_ADD,'-': type.TYPE_OPERATION_SUB,'*': type.TYPE_OPERATION_MUL,'/': type.TYPE_OPERATION_DIV } let type = opMap[token] if (!type) throw `error input: ${token}` result.push({type,value: token, }) 復(fù)制代碼

    4 總結(jié)

    這樣就完成了輸入的處理,這時候,其他的函數(shù)也需要處理一下,應(yīng)為輸入已經(jīng)從字符串變成了tokenize之后的序列了,修改完成之后就是可以calc(parse(tokenize()))完成一整套騷操作了。

    0x005 解決問題3:語法檢測

    語法檢測要解決的問題其實(shí)就是判斷輸入的正確性,是否滿足四則運(yùn)算的規(guī)則,這里用了類似狀機(jī)的思想,不過簡單到爆炸,并且只能做單步判定~~ 定義一個語法表,該表定義了一個節(jié)點(diǎn)后面可以出現(xiàn)的節(jié)點(diǎn)類型,比如,+后面只能出現(xiàn)數(shù)字或者(之類。

    let syntax = {[type.TYPE_NUMBER]: [type.TYPE_OPERATION_ADD,type.TYPE_OPERATION_SUB,type.TYPE_OPERATION_MUL,type.TYPE_OPERATION_DIV,type.TYPE_RIGHT_BRACKET],[type.TYPE_OPERATION_ADD]: [type.TYPE_NUMBER,type.TYPE_LEFT_BRACKET],//... } 復(fù)制代碼

    這樣我們就可以簡單的使用下面的語法判定方法了:

    while (tokens.length) {// ...let next = tokens.pop()if (!syntax[token.type].includes(next.type)) throw `syntax error: ${token.value} -> ${next.value}`// ...} 復(fù)制代碼

    對于(),這里使用的是引用計(jì)數(shù),如果是(,則計(jì)數(shù)+1,如果是),則計(jì)數(shù)-1,檢測到最后的時候判定一下計(jì)數(shù)就好了:

    // ...if (token.type === type.TYPE_LEFT_BRACKET) {bracketCount++}// ...if (next.type === type.TYPE_RIGHT_BRACKET) {bracketCount--}// ...if (bracketCount < 0) {throw `syntax error: toooooo much ) -> )`}// ... 復(fù)制代碼

    0x006 總結(jié)

    • 該文章存在一些問題:
    • 我推導(dǎo)不出為啥要用逆波蘭式,只是知道有這么一個解決方案,拿過來用而已,而不是由問題推導(dǎo)出解決方案。
    • 文字功底不夠,講的不夠 cool。
    • 該實(shí)現(xiàn)也存在一些問題:
    • 并非完全用編譯原理的思想去實(shí)現(xiàn),而是自己摸解決方案,先實(shí)踐,后了解問題。
    • 并沒有參考太多別人的實(shí)現(xiàn),有點(diǎn)閉門造車的感覺。
    • 思考:
    • 對于()的處理或許可以使用遞歸的方式,進(jìn)入()之后重新開始一個新的表達(dá)式解析
    • 思考不夠全,單元測試覆蓋不夠,許多坑還不知道在哪兒

    總之:文章到此為止,有很多不夠詳細(xì)的地方還請見諒,多多交流,共同成長。

    0x007 資源

    • 編譯原理課程
    • 源碼
    • 動畫制作軟件Principle

    轉(zhuǎn)載于:https://juejin.im/post/5c2c7ca56fb9a04a0d56f60b

    總結(jié)

    以上是生活随笔為你收集整理的[前端漫谈] 做一个四则计算器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 丰满人妻综合一区二区三区 | 可以看的av网址 | 久久久精品免费看 | 欧美大黄视频 | 日韩在线高清视频 | 欧美精品在线观看 | 国产免费的av | 懂色av一区二区夜夜嗨 | 精品国产精品国产偷麻豆 | 色玖玖综合| 伦hdwww日本bbw另类 | 国产色综合天天综合网 | 亚洲精品自拍视频 | 男人深夜网站 | 少妇伦子伦精品无吗 | 黄色的网站在线 | 国产欧美一区二区三区在线看蜜臀 | 精品黄色在线观看 | 在线播放精品 | 最新色视频 | 国产妇女视频 | 亚洲精品91在线 | 国产香蕉视频在线观看 | 亚洲精品2| 一区二区日韩在线观看 | 娇小萝被两个黑人用半米长 | 国产精品99无码一区二区 | 特级西西人体4444xxxx | 国产精品久久久久久人妻精品动漫 | 中文字幕久久久 | 无码人妻少妇色欲av一区二区 | 一区二区三区av在线 | 光溜溜视频素材大全美女 | 国产欧美一区二区视频 | 一区二区三区伦理片 | 国产精品中文字幕在线 | 日日综合| 亚洲精品水蜜桃 | 脱美女衣服亲摸揉视频 | 久久久久久亚洲av无码专区 | 伊人网站| 在线观看污污网站 | 久久香蕉综合 | 久久久久久久精 | ass亚洲肉体欣赏pics | 无码人妻丰满熟妇区五十路百度 | 天天干天天草 | 老牛影视少妇在线观看 | 羞羞的网站在线观看 | 国产精品乱轮 | 丝袜美腿中文字幕 | 亚洲麻豆一区二区三区 | 国产三级精品视频 | 人人干人人模 | 刘亦菲毛片一区二区三区 | 麻豆区1免费 | 五月婷婷综合在线观看 | 四虎激情 | 国产精品成人久久 | www.亚洲天堂 | 国产丝袜精品视频 | 日韩videos | 久久91亚洲精品中文字幕奶水 | 久久高清国产 | 嫩草午夜少妇在线影视 | 一卡二卡在线视频 | 美女131爽爽爽做爰视频 | 绯色av一区二区 | 日韩成人免费av | 亚洲少妇第一页 | 亚洲人妻一区二区 | 久久精品爱 | 韩国91视频 | 手机在线成人 | 午夜高潮视频 | 国产原创一区 | 男人的网站在线观看 | 欧美综合第一页 | 在线观看的网站 | 亚洲欧美日韩一区二区三区在线观看 | 久久有精品 | 成人免费大全 | 伊人网在线免费观看 | 国产一区二区视频在线 | 精品福利三区3d卡通动漫 | 精品视频一区二区三区在线观看 | 很黄的性视频 | 天天爽夜夜爽人人爽 | 污导航在线观看 | 成人自拍网站 | 久久精品老司机 | 国产精品三区在线观看 | 国内三级视频 | 五月婷婷小说 | 亚洲欧美bt | 91视频免费观看网站 | 成人a级网站 | 蜜臀精品一区二区三区 | 国产午夜毛片 |