最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【轻聊前端】JavaScript中的数字游戏

    正文概述 掘金(灵感__idea)   2021-03-21   431

    数字,即本篇的主角Number,在程序的世界里能代表很多东西——状态、年龄、价格、计数器等,但由于存储机制的原因,Number并不能很健壮地适应所有场景,就会有一些细节问题,此篇文章,我们沿着Number的属性和方法一起讨论一下数字的应用场景、问题及解决方案。

    再谈基本类型

    在聊变量的那篇文章里,我们说JavaScript中原本有5种基本类型,加上ES6之后引入的Symbol(符号),是6种,但是,还有一种“预备役”的类型——BigInt,目前应该是在“建议推荐标准阶段”,可以按其字面意思理解叫“大整数”。

    说“大整数”,得先看看Number,JavaScript中是没有其他语言中的“int、float、double”这些类型的,统一为定义一个Number类型的数字,那么,是因为它不够大?还真是。

    精度的“陷阱”

    大数

    Number能够表示的最大数字是 2的53次方 - 1。咱先看看这个数是多少:

    9007199254740991

    使用 Number.MAX_SAFE_INTEGER 即最大的安全整数也可获得这个值。

    类似地,Number中有这么几个值:

    Number.MAX_VALUE // 1.7976931348623157e+308
    Number.MAX_SAFE_INTEGER // 9007199254740991
    Number.MIN_VALUE // 5e-324
    Number.MIN_SAFE_INTEGER // -9007199254740991
    

    乍一看,已经挺大了,用正常的计数法已经读不出来,一旦超过这个值,就会被转换为科学计数法,形如:1.79e+308,且精度上会有误差。

    问题来了,什么时候会用这么大的数?比如,前后端进行Long型数据传递的时候,就可能出现这种情况,而且这种情况是需要处理的,在之前,一种常见的方法是把它转换为字符串,逐位进行计算,之后再组合。一些专门用于大数处理的库也是这么做的,比如:big.js

    有了BigInt类型之后,就可辅助处理这种情况。比如,可以像下面这样定义:

    10n,或者调用函数BigInt()
    

    它类似Number,但也有一些不同,它不能用 Math 对象中的方法;也不能和任何 Number 实例混合运算,两者必须转换成同一种类型。当然,还有一件自然的事情,就是带小数的运算会丢掉小数部分取整。

    说了这么多,看看它能够发挥的作用吧。

    let testInt = 9007199254740991;
    testInt * 1000000   // 9.007199254740991e+21
    BigInt(testInt * 1000000)  //  9007199254740990951424n
    

    可以看出,在进行BigInt处理后,数字不再会被“科学化”。但这个类型不能乱用,只有当确定数字会超过 2的53次方时才用,且不宜进行 Number 装换。

    小数

    既然会有精度问题,涉及大数就很可能涉及小数。比如大家喜闻乐道的“0.1 + 0.2”的问题。

    我们在控制台输入这个表达式,会发现:

    0.1 + 0.2 //    0.30000000000000004
    

    结果并不是0.3,而是很长的一串,末尾还有个4。

    不熟的可能想不到,对人来说很容易的运算,计算机居然出错了,什么道理?简要解释一下:

    计算机有能够表示的最大值,也有能表示的最小值(大于零的),即上文提到的 Number.MIN_VALUE(5e-324) 。

    计算机中的数字都是以二进制存储的,所以要先将 0.1 和 0.2 转化成二进制,对于十进制转二进制,整数部分除二取余,倒序排列,小数部分乘二取整,顺序排列。

    0.1 转化为二进制 0.0 0011 0011 0011 0011 0011 0011 ... (0011循环)

    0.2 转化为二进制 0.0011 0011 0011 0011 0011 0011 0011 ... (0011循环)

    接下来就是两个二进制数的计算。

    二进制数之间的计算规则是:进位“逢二进一”,借位“借一当二”。

    最终得到的二进制数是0.010011001100110011001100110011001100110011001100110100

    然后又要转成十进制。

    二进制转十进制的方法是:小数点后第一位 *2 ^ -1,第二位 *2 ^ -2,以此类推。

    转换后结果是0.30000000000000004,即我们之前看到的值。

    既然只要数字够小就会出问题,同理的,“0.3 - 0.2”也有问题,会得到 0.09999999999999998。

    光知道问题不行,要解决。

    • 方法一:

    既然数字小的情况下出的问题,变大再缩小不就行了?这和CSS中单像素之类效果的实现如出一辙。

    (0.1*1000+0.2*1000)/1000   // 0.3
    
    • 方法二:

    ES6后,Number新增了一个属性——Number.EPSILON,两个“可表示数”之间的最小间隔。为数字之间的误差提供了一个范围,我们打印出来看看:

    Number.EPSILON   // 2.220446049250313e-16
    

    这个值刚好等于

    2**-52    // 2.220446049250313e-16
    

    你可能想到了,仅仅一个范围并不能使得“0.1 + 0.2 == 0.3”真正成立,而是让“忽略误差”之后的布尔成立。

    let a = 0.1 + 0.2;
    let b = 0.3;
    Math.abs(a-b)<Number.EPSILON  // true
    

    虽然不是真正解决,但这也提供了一种变相成立的情况是吧。

    词法“误会”

    已经聊了两个问题,索性继续聊下去,前面的文章里,提到过JavaScript中的一个常见方法,toString(),用于将一切具备这个方法的类型的值转为字符串类型的值。通常情况下,都能得到预期的结果。比如:

    let a = 1;
    a.toString()  // “1”
    

    但是,如果写成:

    1.toString()
    

    这就有点纠结了,按照书写上的意愿,我们是想把1转换成字符串,但是按照数字的看法,“1.”也能理解为小数的前半部分,这怎么弄。

    这就涉及到了词法规则,计算机对于程序,或者说字符的读取,并不会完全按照人的“真正意图”来进行,而是它读到的内容只要符合规则,就按规则来。

    JavaScript当中的最小语义单元叫“词”,只要符合词的规则,就构成词。

    我们知道,代码当中有空白、换行、注释,然后就是我们写的有具体含义的代码了。

    JavaScript词的规则中,十进制的 Number 可以带小数,小数点前后部分都可以省略,但是不能同时省略

    .01
    1.
    1.01
    

    以上几种写法都是合法的。

    那么“1.toString()”中的“1.”就会被当做省略了小数点后面部分的数字来处理,自然就不会得到正确的结果,而是报错。

    那正确结果的写法是什么呢?以下两种均可:

    1 .toString()  //中间隔了个空格
    1..toString()  //中间多加一个点符号
    

    回正轨

    在文章开头,为了引入 BigInt,讲了个精度的问题,就直接铺开讲了几个常见问题,从这段开始正常聊聊Number中最常见的属性和方法。

    toString()

    上面刚看过toString,咱就趁热打铁接着聊。

    toString是很多对象的原型上都会有的通用方法,但在通用的前提下,每种类型可能有自己独特的作用,比如,Number类型的toString支持传入一个参数(radix),表示以“几进制”来转换数字。

    let a = 10;
    a.toString()   // "10"  不传值的时候默认“十进制”
    a.toString(2)  // "1010"  传了2,就会转换成“二进制”
    a.toString(8)  // "12"  传了8,就会转换成“八进制”
    

    上面代码已经能够简单看到效果了,但进制的总体要比我们常用的更多。比如,我们所熟知的,CSS当中的色值,就有十六进制表示。#FFF 表示白色。所以,如果转换的基数大于10,则会使用字母来表示大于9的数字。

    a.toString(16)  //"a"   嗯,这真的是个巧合~
    

    parseInt()/parseFloat()

    这两个方法就很好理解了,Number上的这两个方法和全局对象的方法没有不同,通常用于将字符串转换为数字。

    看一下效果:

    let a = "1.5";
    parseInt(a);   // 1
    parseFloat(a);  // 1.5
    

    但它的处理机制不止这么简单,再看

    let a = "1.5b";
    parseInt(a);  // 1
    parseFloat(a); // 1.5
    

    当处理的字符串是“数字+其他字符”的时候,它们会把后面的值给砍掉,返回前面的数字。

    这在一些需要输入数字的场景下很有用,比如,有个表单输入框的价格,限制只能输入数字,而不能输入其他字符,就派上了用场。

    凡事有个但是,如果字符加在了前面呢?

    let a = "c1.5b";
    parseInt(a);  // NaN
    parseFloat(a); // NaN
    

    这就真的无能为力了...

    慢着,前面好像说到限制表单输入,你说,我何必用这些方法,我用input的 number 类型不就完了么。理想很丰满,现实很骨感,input的 number 交互体验不完美不说,它绑定的值也是被处理成字符串的,而不是 Number。

    除了限制输入只能是数字之外,有时还会限制输入几位小数,怎么办?不用怕,也是有方法的。

    toFixed()

    toFixed()就是用来保留小数位数的,称为“定点表示法”,直接看:

    let a = 1.5;
    a.toFixed()  // "2"
    

    嚯,这一看不打紧,发现两个问题。

    不传值默认不保留小数位,这很明显就不说了。重点在于另外两项:

    • 返回值是字符串类型,而不是数字
    • 当小数位为5(或者大于5),结果会进行四舍五入。

    可以再验证一下。

    let a = 1.4;
    a.toFixed()  // "1"
    

    第一点还没什么,稍加注意就好,第二点显然就会出问题,如果你限制输入两位小数,在输入框里输入“14.56”,然后多输了一个数字“14.565”,就会被处理成“14.57”,看起来一个数字之差,也是不该被允许的。所以,在便利之余,这是它的一点小瑕疵。

    如何处理呢,还记得前面我们处理“0.1+0.2”问题的方法吗?用在这个地方更合适,即,保留几位小数,就先乘以“10的n倍”,再除以“10的n倍”,就可以得到原本的数字了。

    toPrecision()

    跟toFixed()效果类似的一个方法是保留值的精度,或者换个说法,指定数字的有效位数

    let a = 1.4;
    a.toPrecision(2) // "1.4"
    a.toPrecision(1) // "1"
    

    它也有个同样的问题,就是四舍五入,会把“1.6”保留为“2”,但作为保留几位数的常规定义来说,好像又是合理的,所以,只需要在使用的时候注意一下就好。

    至此,常用的数字处理方法介绍差不多了,再看两个数字判断的方法。

    数字判定

    isNaN()

    做程序处理的时候,总会遇到异常情况,预期是数字,或者能转换成数字的,如果真的不能,就可能是 NaN,这时候,用其他的值或者方法来判断NaN是不凑效的,isNaN()方法就派上用场了。

    Number.isNaN(Number('a'))  // true
    

    isInteger()

    某些数值只适合用整数表示,需要有个整数判定方法,如果用常规方法,可以先看是不是数字,然后看是不是小数,而用isInteger()只需一步。

    let a = 1.4;
    Number.isInteger(a)  // false
    

    Math

    说完Number数字本身的属性和方法,该来看一下另一个重要角色了,即内置的Math对象。

    Math的属性和方法多且强大,我们挑几个常用的说说。

    Math.PI

    大家知道,PI是数学中的圆周率,读书的时候,我们使用的都是圆周率的近似值“3.14”,而Math方法给我们提供了现成的属性可直接调用Math.PI。

    既然是圆周率,当然是会用在圆或者圆弧的场景,比如画一个圆。

    也经常会涉及弧度和角度的转换,弧度除以 (Math.PI / 180) 可转换为角度,同理,角度乘以这个数则能转换为弧度。

    Math.abs(x)

    这个方法在上面已经见到过,我们在比较两个数值的时候,如果只需知道它们的差值,而不在乎谁大谁小,就可用此法。

    Math.round(x)

    前面聊保留位数的时候,多次提到“四舍五入”,本尊终于出现了。

    Math.round(1.4)    // 1
    Math.round(1.6)    // 2
    

    “四舍五入”本身不是问题,只是某些场景的默认处理不合适罢了,这个算法是很经典的算法,在很多方面都能发挥作用,虽然如此,还有一种它不适合的场景,比如下一位。

    Math.ceil(x)/Math.floor(x)

    有时候,我们需要对计算的结果做取整处理,比如返回的小数有“1.4、1.6”,如果大于1的都按2处理,或者小于2的都按1处理,就要用到取整算法。

    //向上
    Math.ceil(1.4)    // 2,下同
    Math.ceil(1.6)
    
    //向下
    Math.floor(1.4)   // 1,下同
    Math.floor(1.6)
    

    初记可能总记不住,只需要记得floor有“地面、地板”的意思,自然就是向下了~

    Math.max()/Math.min()

    如果对Math对象不熟悉,碰到一组数据需要取到它们当中的最大或者最小值,你可能会写一个逐项比较的算法取结果,但这样复杂度是最高的,显得不太划算,有这么个好方法就要善用。

    let a = [1,2,3,4,5];
    Math.max(...a)   // 5
    Math.min(...a)   // 1
    

    PS:此方法只能直接处理数字,此处用展开运算符将数组中的数值进行了展开处理。

    Math.pow()

    这个方法前面已经见过面了,求数值的多少次方。而且我们也提过,现在有了新的运算符来做这件事,即’**‘。

    比如2的3次方可写成“2**3”,不再赘述。

    Math.random()

    Math中最后一个常用方法就是random(),也就是“随机数”。

    很多时候,有规律会显得整齐、美观,但有些时候无规律更显自然和多样。

    random()方法本身只会返回从0到1中间的一个数字,但如果发挥想象,它就能做很多有用的事。

    比如,0到1可理解为比例,百分之几,那么就可以输出任意两个数中间的数,先算出差值,再用随机数乘以差值,加上其中一方。

    w = m - n;
    Math.random()*w + n
    

    或者,有时我们需要进行无规律的重复,球弹的高度,雪花飘落的速度和距离等,就可以先定一个值,再用随机数与之相乘,就能控制从0%到100%不同范围的值随机出现。

    至此,Math对象的介绍也告一段落,Math中还有很多其他方法,比如三角函数等,鉴于使用场景比较特殊,这里就不详述,如有需要,可自行深入研究。

    总结

    编程中数字的话题,说小也小,小到我们使用它的时候就顺手一写的事儿,但说大也大,拿之前说的输入框限制输入几位小数,就需要把每种输入的可能性都考虑到才能做到没有bug,终归,编程是个细活儿,不论工具箱有多丰富和强大,还是要使用它的人思路清晰、思维缜密,才能写出bug更少的代码。

    这是第五篇了,行程将将过半,后面也会逐渐进入深水区,为大家揭开一些看似复杂概念的神秘面纱,我们继续加油!~

    博文链接:

    【轻聊前端】JavaScript中的数字游戏

    系列文章:

    【轻聊前端】打好基本功,跟我轻松学原生

    【轻聊前端】小角色,大用途——变量

    【轻聊前端】为什么说一切皆对象?

    【轻聊前端】那些“无理取值”的运算


    起源地下载网 » 【轻聊前端】JavaScript中的数字游戏

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元