时间:2022-09-16 10:16:29 | 栏目:JavaScript代码 | 点击:次
注:只是前端实现
需求来源是因为有一个做嵌入式 C/C++的基友做了一个远程计算器。 需求是要求支持输入一个四则混合运算公式的字符串,返回计算后的结果。
想看看用 C/C++封装过的 JavaScript 如果要实现这样一个功能最终效果(文章后我会讨论这两种实现思路,还望各位看官可以提出一些优化方案以及建议之类的~)。
主要是用到字符串切分为数组的方法 split、以及数组中插入删除的方法 splice。 字符串正则 replace 方法是考虑到用户的输入习惯可能有所不同,例如 1+2*3/4 与 3 * 7 + 229。
支持:
不支持:
/** * js四则混合运算计算器 功能实现(约20行+ 面条代码) * @param {string} str 输入的四则运算字符串 * @return {number} 输出 结果 */ const calculator = (str) => { // 定义添加字符函数 const add = (arr, symbol) => { let length = arr.length; while (length > 1) { arr.splice(length - 1, 0, symbol); // 在每一项后面添加对应的运算符 length--; } return arr; // 目的是得到一个改变长度的数组 } const array = add(str.replace(/\s*/g,"").split('+'), '+').map(it => add(it.split('-'), '-').map(it => add(it.split('*'), '*').map(it => add(it.split('/'), '/')))).flat(3);; // 先运算乘除法 ['*', '/'].map(it => { while (array.includes(it)) { const index = array.findIndex(o => o === it); index > 0 && it === '*' ? array.splice(index - 1, 3, (Number(array[index - 1]) * Number(array[index + 1]))) : array.splice(index - 1, 3, (Number(array[index - 1]) / Number(array[index + 1]))); } }) // 再执行加减法,即从左至右的计算 while (array.length > 1) { array[1] === '+' ? array.splice(0, 3, (Number(array[0]) + Number(array[2]))) : array.splice(0, 3, (Number(array[0]) - Number(array[2]))); } return Number(array[0]).toFixed(2); }
如果对 ES6 语法还算熟悉的话,应该可以轻松阅读代码的。 想必你也注意到了,这也是其中令我比较纠结的:在日常开发中,是否该经常写一些面条代码呢?
(轻松理解的大佬可以直接跳到:步骤3)
1.实现最基础的加减乘除
2.支持高位数的运算
3.支持多次的运算
4.支持...
如果是初学者,建议跟着敲一下过程(或者 f12 验证 + 调试),编程能力某种角度下一定是建立在代码量之下的。
// 版本一 const calculator = ((str) => { // 定义最基础的加减乘除 const add = (a, b) => a + b; const sub = (a, b) => a - b; const mul = (a, b) => a * b; const div = (a, b) => a / b; // 将输入的字符串处理为 数组 const array = str.split(''); // **【处理基本四则运算】 ['*', '/', '+', '-'].map(it => { const index = array.findIndex(o => o === it); if (index > 0) { switch (it) { case '*': array[index + 1] = mul(array[index - 1], array[index + 1]); break; case '/': array[index + 1] = div(array[index - 1], array[index + 1]); break; case '+': array[index + 1] = add(Number(array[index - 1]), Number(array[index + 1])); break; case '-': array[index + 1] = sub(Number(array[index - 1]), Number(array[index + 1])); break; } array.splice(index - 1, 2) } }) // return array[0]; console.log('返回值:', array[0]); })('3+6*5/6-3') // 返回值: 5
这样就实现了一个四则混合运算的计算器!
但是这个计算器很鸡肋,只是一个最基础的功能上的实现。即:只可以运行一位数数字的加减乘除混合运算。
其实第一步的想法是,利用数组的性质,通过操作数组来操作单次的四则运算。其中数组的遍历,我优先 *, / 法,紧接着是 +,- 法。 这其实是有问题的,乘除法在实际运算中的优先级并不明显,可以说是不怎么影响运算的结果(在文章最后一个版本实现涉及到性能上的讨论时会详谈),但是加减法就会有影响了:必须是从左至右的实现,否则影响运算的结果(这里不多赘述)。
【处理基本四则运算】
首先处理字符串为数组 const array = str.split('');这一步代码举例说明:
(图一)
(图二)
可以看到每次打印都会打印初始数组以及通过 splice 方法处理之后的结果。
弊端:此版本不支持多次运算,即四则混合运算只能执行一次。同时,也不能够支持高位运算。
在图一中
如果是涉及高位(个位以上)的数值运算字符串的话,单纯的使用 split('') 方法会把两位数数值,处理成数组的两项,即影响运算结果。
所以我需要一个方法,在接收一个字符串以后,得到我想要的字符串:
(图三)
如图三所述, ary 即所需。
所以,图三中由 str 到 ary 的过程就是本次版本所需要实现的:
/** * 实现字符串的数组化分割 * @param {string} strs 输入的字符串 : '12*33/3+9+10' * @returns 数组 ['12', '*', '33', '/', '3', '+', '9', '+', '10'] */ const split = () => { const result = str.split('+') // 遇到 + 处理为数组 .map(it => { return it.split('-') // 遇到 - 处理为数组 .map(it => { return it.split('*') // 遇到 * 处理为数组 .map(it => { return it.split('/') // 遇到 / 处理为数组 }) }) }) return result.flat(3); }
我在设计这个算法的时候,一时间也没有太好的思路和想法,该函数处理字符串为一个多维数组,然后再将数组扁平化处理。如图四所示:
(图四)
图四中,执行该函数,得到一个多维数组(其实最高也只有三维数组),返回值 result 打印出来的结果可以看到,基本满足所需要的数组:['31', '+', '62', '*', '5', '/', '6', '-', '3'] 。
接下来,为其带上运算符:
/** * 定义添加字符函数 * @param {string[]} result 传入的数组 ['31', '62*5/6-3'] * @param {string} symbol 传入的运算符 * @returns 数组 ['31', '+', '62*5/6-3'] */ const add = (result, symbol) => { let length = result.length; while (length !== 1) { result.splice(length - 1, 0, symbol); // 在每一项后面添加对应的运算符 length--; } return result; // 目的是得到一个改变长度的数组 }
比如传入 ['31', '62*5/6-3'] ,只需要在第一项之后补 '+' 即可。
实现的目的是考虑到多次运算的时候,为每一个因为 '+' 分割的数组中的项添加运算符,所以这里用到了 while 循环语句,并且由一个变量 length 控制(也可以遍历数组或者 for 循环数组实现这一步操作);
检验结果,如图五所示:
(图五)
这样就实现了这个任意长度数值数组输入时,返回带符号的数组。
【回顾一下】:
上面两个函数的整体实现就是,实现了根据符号分割数组,根据传入的数组与符号添加符号:
结合两个函数,并且简化一下代码(其实我个人还是喜欢写面条代码的,只是可能不利于阅读,但是看起来舒服一些~):
// 定义添加字符函数 const add = (result, symbol) => { let length = result.length; while (length !== 1) { result.splice(length - 1, 0, symbol); // 在每一项后面添加对应的运算符 length--; } return result; // 目的是得到一个改变长度的数组 } const array = (strs = str) => add(strs.split('+'), '+').map(it => add(it.split('-'), '-').map(it => add(it.split('*'), '*').map(it => add(it.split('/'), '/') ) ) ).flat(3);
即,任意运算字符串的传入都可以处理为所需数组如图六所示:
(图六)
array 函数在后面直接把内部处理函数的返回值绑定了。
对于上述算法的设计如果有更好的实现还希望有朋友可以指出,大家互相之间可以学习一下。
回到版本一,目前的实现只支持一次的四则混合运算,更合理的实现应该是先运算乘除法,再运算加减法,而且先出现的先执行。
完整运算代码:
const calculator = (str) => { const add = (result, symbol) => { let length = result.length; while (length > 1) { result.splice(length - 1, 0, symbol); length--; } return result; } const array = add(str.replace(/\s*/g, "").split('+'), '+').map(it => add(it.split('-'), '-').map(it => add(it.split('*'), '*').map(it => add(it.split('/'), '/')))).flat(3);; // 先运算乘除法 while (array.includes('*') || array.includes('/')) { const itSymbol = array.find(o => o === '*' || o === '/'); const index = array.findIndex(o => o === '*' || o === '/'); index > 0 && itSymbol === '*' ? array.splice(index - 1, 3, (Number(array[index - 1]) * Number(array[index + 1]))) : array.splice(index - 1, 3, (Number(array[index - 1]) / Number(array[index + 1]))); } // 再执行加减法,即从左至右的计算 while (array.length > 1) { array[1] === '+' ? array.splice(0, 3, (Number(array[0]) + Number(array[2]))) : array.splice(0, 3, (Number(array[0]) - Number(array[2]))); } return Number(array[0]).toFixed(2); }
注:有必要说明一下,因为个人习惯不同,所以输入带有空格情况,所以这里在处理字符串之前首先用到了一个正则表达式 str.replace(/\s*/g, "") (去除空格)。
等等,我刚刚想到了什么?
如果大家都在输入的时候,自觉加一个空格隔开运算符与数值的话~
是不是我之前版本二中的字符串处理就可以省一下啦!!
所以作为开发者,一定要 注意规范,注意规范,注意规范!
上面完整代码中,
其实这段代码更符合数学思维,先运算乘除法(谁在前先运算谁),再运算加减法。
如果大家有一些其他的想法,可以一起讨论一下~
1. 实现逆波兰表达式
1+2*3 这是一个中缀表达式,人脑很容易计算,结果为7。当然计算机也很容易处理这个表达式。
当我们输入1.2+(-1+3*1)*2,人脑需要思考一下,但计算机还是可以通过固定代码快速计算出结果。
但是,当我们随机输入中缀表达式 XXX 时,人脑可以手动计算出结果,计算机不可能一个表达式一个代码块,那么计算机怎么实现通用且快速的计算呢?答案就是后缀表达式。
中缀和后缀表达式在数据结构里有涉及到,我就不讲概念了,下面手动模拟一下计算机计算字符串表达式的过程。
2. 中缀表达式 => 后缀表达式
计算机易于计算的其实是后缀表达式,整个过程就是将已知的中缀表达式转换为后缀表达式。
2.1 定义【操作数栈】和【运算符栈】:
2.2 运算符栈出栈,操作数栈入栈,上式即可成为: 123*+ 这就是一个简单的后缀表达式
2.3 计算机在运算后缀表达式时:运算符栈读取 *,操作数栈读取 2,3 得到结果 6,;然后运算 1 + 6 = 7。
3. 较复杂的表达式计算
入栈:
(
后的-
作为负数进入操作数栈(如果作为符号位,后面计算会成1.1 - 30
);
与上文一样,只是不同之处在于运算符栈,遇到 (
以后先进入运算符栈;
直到遇到 )
3.1 使用 #
符号区分 负数、高位数、以及符号位
3.2 所以得到的后缀表达式为: #-1.1#3#10#*#+#2#/#
;
出栈过程:
#
为准,依次取出-1.1 3 10
-1.1 30
我拿到【实现一个支持四则混合运算的计算器】需求以后,首先想到的是字符串转数组,然后去操作数组,然后由于高级语言的特性,很多方法已经封装完成,所以实现起来相对容易一些。
当然,也可以采用前端的代码,用着后端的思维去实现也是一个选择。
其实这个计算器与电脑中的常规计算器并无区别,后期可以考虑的升级方式
(
与 )
的优先级功能;总结一下就是,后端的实现在性能上无与伦比,尤其是代码的执行速度上,我这里没有测试数据,但是如果你有刷力扣的话,你可以看看同样的算法,JS 的空间复杂度【内存消耗】,是 C/C++ 等更底层的语言消耗数倍。
同样的,如果用 C/C++ 底层语言 + 后端思维 去实现【开辟内存】、将中缀表达式转换为后缀表达式所用定义的【操作数栈】、【运算符栈】;以及各种栈顶栈底的【指针操作】;外加如果交由用户使用涉及到的设置【代理】,网络协议封装等等... (最终总代码量数百行)
我将之称为业务复杂度(hhh)对比前端 20行+ 的代码实现~
更底层语言需要考虑的东西比较多,所以实现起来花费的人力相对更多,同样的收获的对电脑性能消耗性价比也是前端 JS 不可比拟的
不当之处还望各位指正~