页面统计时间问题解决过程

2018-10-15 17:38 我说呢

背景交代下:百度地图项目中一个H5的活动页面,需要统计下用户的PV、UV、页面停留时长等数据,以便验收运营效果,调整策略。

其实问题主要在于统计页面的停留时间上;本以为就是一个在开始进入的时候发送一个ajax请求,记录下时间,在退出监听window.onbeforeunload事件发送一下请求就搞定了。

第一版代码实现:

window.onunload = window.onbeforeunload = function (e) {
    $.ajax({...});
}

然而,结果并不那么如意:在PC端成功了,在Mobile上基本上没有统计到结束的时间。发现被浏览器cancel掉了,原来是异步的问题,然后就改成同步的。

var request = new XMLHttpRequest();
request.open('GET', url, false); // 同步请求
request.send(null);
伤心.png

测试下,iPhone可以了,安卓机又不行了(郁闷.png)。经过一番调试发现:

安卓机在百度地图app退出H5页面时,并不会触发onbeforeunload,应该是并不会把当前webview销毁,而是在下次重新进入一个H5页面的时候会销毁上一个(触发上一个H5的onbeforeunload事件)

机智.png

发现根本没有触发ajax,经过一顿google后,发现另一个事件,visibilitychange:

document.addEventListener('visibilitychange', function() {
  console.log( document.visibilityState );
});

但是这个事件的弊端是,如果app只是退出到后台运行(H5页面并没有关闭),也会执行该事件。

ganga.png

所以,带来的就是不管退出到后台还是真正的关闭webview,都会发送ajax,说 我结束了,其实有的时候并没有结束。

还有一点优化的就是,如果用户退出到后台会造成统计时间不准,因为你真正的退出的时候会再次发一次,这时候中间可能有很长时间并没有看当前页面,需要记录下退出到后台的时间,然后去除掉就可以了。

最后的伪代码:

if (navigator.userAgent.toLowerCase().indexOf('android') > 0) {
    document.addEventListener('visibilitychange', function () {
        if (document.visibilityState != 'hidden') {
            ...sync ajax...
        }
    });
}
else {
    window.addEventListener('pagehide', function () {
        ...sync ajax...
    }, false);
}

其中pagehide与onbeforeunload差不多(反正没找到不同)。

另外,中间还用了一个不太好的,就是使用了轮询机制(心跳机制)。弊端是不管关没关闭都会触发,另外统计时间不是那么准确。

参考资料:visibilitychangePageVisibilityAPI

关于elementUI scope 设置th样式

2018-09-27 14:53 我说呢
vueloader

关于这个问题,困扰了有一段时间了,上次写东西就发现了这个问题,当时比较着急就写了全局的样式,后来给忘了(惭愧.gif)。

今天看了下官网,里面专门有对scoped的css的说明vue-loader官网

下面说明下原因:

使用scoped后的style标签会在vue-loader里进行处理。这里所谓的局部样式就是在你当前组件里的所有html标签打一个data-tag。

例如,如果你在vue文件中这样编写css:

<style lang='stylus' scoped>
    .home {
        .el-table th .cell {
            background-color: red;
        }
    }
</style>

编译后的css样式如下:

.home .el-table th .cell[data-v-03b663c3] {
  background-color: red;
}

然后在elementUI中el-table中你的一个子组件,vue-loader在编译中对于子组件只对子组件的根元素打上tag,不会对th以及之下的div.cell打上tag,所以样式寻找中找不到这个,那就注定不生效了。

使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式 --- 来自vue-loader官网

解决方法

深度作用选择器

如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>> 操作符:

<style scoped>
.a >>> .b { /* ... */ }
</style>

编译后:

.a[data-v-f3f3eg9] .b { /* ... */ }

搞定。。

mac 如何管理多个公钥、私钥

2018-03-27 22:55 奔跑的搬砖工

当有多对公/私钥对时,管理和操作会变复杂。使用 ssh 时需要通过 -i 指定私钥文件,比较麻烦。可以通过在 .ssh 目录下新建一个 config 文件来进行管理。

img

1.新增ssh的配置文件,并修改权限(如果有报错就修改权限)。

touch ~/.ssh/config
chmod 600 ~/.ssh/config

2.配置内容如下(还有别名,端口等参数,不多做介绍)

Host *.xxxxxx.xxx  
    IdentityFile ~/.ssh/xxxxxxxx
    User xxxxx  

Host github.com  
    IdentityFile ~/.ssh/id_rsa_for_github  
    User git  

3.搞定

building for production...Killed

2018-03-06 17:35 奔跑的搬砖工

这个已经遇到过两次了,是时候记录下了

在vue项目中运行npm run build的时候报错了

img

原理

按照他人的说法是,服务器内存不够用了,这样就给他配置一个单独的内存出来就解决了

解决方法

 sudo /bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=1024
 sudo /sbin/mkswap /var/swap.1
 sudo /sbin/swapon /var/swap.1

vue-cli webpack css 图片路径

2018-03-05 15:17 我说呢

图片放在了assets文件夹下,img和background-image引用都用相对路径,build的时候设置assetsPublicPath: './',打包出来后发现background-image的路径出现了问题

解决方法 (the first)

1.先在data里面导入这张图片,例如:bg:require(‘./openIndexBG2.jpg’)

2.然后在template里面对需要background-image属性的DOM做个绑定,例如::style=“{backgroundImage: ‘url(‘ +bg + ‘)’}“

这个方法确实是可以解决图片路径的问题,不过有的时候是不能直接写style样式的就不生效了

解决方法 (the second)

在build的utils.js文件中添加一行:

webpack_build_utils

publicPath根据项目目录配置

转: 你所忽略的js隐式转换

2018-02-06 10:44 奔跑的搬砖工

你有没有在面试中遇到特别奇葩的js隐形转换的面试题,第一反应是怎么会是这样呢?难以自信,js到底是怎么去计算得到结果,你是否有深入去了解其原理呢?下面将深入讲解其实现原理。

其实这篇文章初稿三个月前就写好了,在我读一些源码库时,遇到了这些基础知识,想归档整理下,就有了这篇文章。由于一直忙没时间整理,最近看到了这个比较热的题,决定把这篇文章整理下。

const a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}
if (a == 1 >> a == 2 >> a == 3) {
  console.log('hello world!');
}

网上给出了很多不错的解析过程,读了下面内容,你将更深入的了解其执行过程。

1、js数据类型

js中有7种数据类型,可以分为两类:原始类型、对象类型:

基础类型(原始值):

Undefined、 Null、 String、 Number、 Boolean、 Symbol (es6新出的,本文不讨论这种类型)

复杂类型(对象值):

object

2、三种隐式转换类型

js中一个难点就是js隐形转换,因为js在一些操作符下其类型会做一些变化,所以js灵活,同时造成易出错,并且难以理解。

涉及隐式转换最多的两个运算符 + 和 ==。

+运算符即可数字相加,也可以字符串相加。所以转换时很麻烦。== 不同于===,故也存在隐式转换。- * / 这些运算符只会针对number类型,故转换的结果只能是转换成number类型。

既然要隐式转换,那到底怎么转换呢,应该有一套转换规则,才能追踪最终转换成什么了。

隐式转换中主要涉及到三种转换:

1、将值转为原始值,ToPrimitive()。

2、将值转为数字,ToNumber()。

3、将值转为字符串,ToString()。

2.1、通过ToPrimitive将值转换为原始值

js引擎内部的抽象操作ToPrimitive有着这样的签名:

ToPrimitive(input, PreferredType?)

input是要转换的值,PreferredType是可选参数,可以是Number或String类型。他只是一个转换标志,转化后的结果并不一定是这个参数所值的类型,但是转换结果一定是一个原始值(或者报错)。

2.1.1、如果PreferredType被标记为Number,则会进行下面的操作流程来转换输入的值。

1、如果输入的值已经是一个原始值,则直接返回它
2、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
   如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
3、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。

2.1.2、如果PreferredType被标记为String,则会进行下面的操作流程来转换输入的值。

1、如果输入的值已经是一个原始值,则直接返回它
2、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
3、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
   如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。

既然PreferredType是可选参数,那么如果没有这个参数时,怎么转换呢?PreferredType的值会按照这样的规则来自动设置:

1、该对象为Date类型,则PreferredType被设置为String
2、否则,PreferredType被设置为Number

2.1.3、valueOf方法和toString方法解析

上面主要提及到了valueOf方法和toString方法,那这两个方法在对象里是否一定存在呢?答案是肯定的。在控制台输出Object.prototype,你会发现其中就有valueOf和toString方法,而Object.prototype是所有对象原型链顶层原型,所有对象都会继承该原型的方法,故任何对象都会有valueOf和toString方法。

先看看对象的valueOf函数,其转换结果是什么?对于js的常见内置对象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function。

1、Number、Boolean、String这三种构造函数生成的基础值的对象形式,通过valueOf转换后会变成相应的原始值。如:

var num = new Number('123');
num.valueOf(); // 123
var str = new String('12df');
str.valueOf(); // '12df'
var bool = new Boolean('fd');
bool.valueOf(); // true

2、Date这种特殊的对象,其原型Date.prototype上内置的valueOf函数将日期转换为日期的毫秒的形式的数值。

var a = new Date();
a.valueOf(); // 1515143895500
3、除此之外返回的都为this,即对象本身:(有问题欢迎告知)
var a = new Array();
a.valueOf() === a; // true
var b = new Object({});
b.valueOf() === b; // true
再来看看toString函数,其转换结果是什么?对于js的常见内置对象:Date, Array, Math, Number, Boolean, String, Array, RegExp, Function。

1、Number、Boolean、String、Array、Date、RegExp、Function这几种构造函数生成的对象,通过toString转换后会变成相应的字符串的形式,因为这些构造函数上封装了自己的toString方法。如:

Number.prototype.hasOwnProperty('toString'); // true
Boolean.prototype.hasOwnProperty('toString'); // true
String.prototype.hasOwnProperty('toString'); // true
Array.prototype.hasOwnProperty('toString'); // true
Date.prototype.hasOwnProperty('toString'); // true
RegExp.prototype.hasOwnProperty('toString'); // true
Function.prototype.hasOwnProperty('toString'); // true
var num = new Number('123sd');
num.toString(); // 'NaN'
var str = new String('12df');
str.toString(); // '12df'
var bool = new Boolean('fd');
bool.toString(); // 'true'
var arr = new Array(1,2);
arr.toString(); // '1,2'
var d = new Date();
d.toString(); 
var func = function () {}
func.toString(); // function () {}

除这些对象及其实例化对象之外,其他对象返回的都是该对象的类型,(有问题欢迎告知),都是继承的Object.prototype.toString方法。

var obj = new Object({});
obj.toString();
Math.toString();

从上面valueOf和toString两个函数对对象的转换可以看出为什么对于ToPrimitive(input, PreferredType?),PreferredType没有设定的时候,除了Date类型,PreferredType被设置为String,其它的会设置成Number。

因为valueOf函数会将Number、String、Boolean基础类型的对象类型值转换成 基础类型,Date类型转换为毫秒数,其它的返回对象本身,而toString方法会将所有对象转换为字符串。显然对于大部分对象转换,valueOf转换更合理些,因为并没有规定转换类型,应该尽可能保持原有值,而不应该想toString方法一样,一股脑将其转换为字符串。

所以对于没有指定PreferredType类型时,先进行valueOf方法转换更好,故将PreferredType设置为Number类型。

而对于Date类型,其进行valueOf转换为毫秒数的number类型。在进行隐式转换时,没有指定将其转换为number类型时,将其转换为那么大的number类型的值显然没有多大意义。(不管是在+运算符还是==运算符)还不如转换为字符串格式的日期,所以默认Date类型会优先进行toString转换。故有以上的规则:

PreferredType没有设置时,Date类型的对象,PreferredType默认设置为String,其他类型对象PreferredType默认设置为Number。

2.2、通过ToNumber将值转换为数字

根据参数类型进行下面转换:参数结果undefinedNaNnull+0布尔值true转换1,false转换为+0数字无须转换字符串有字符串解析为数字,例如:‘324’转换为324,‘qwer’转换为NaN对象(obj)先进行 ToPrimitive(obj, Number)转换得到原始值,在进行ToNumber转换为数字

2.3、通过ToString将值转换为字符串

根据参数类型进行下面转换:参数结果undefined’undefined’null’null’布尔值转换为’true’ 或 ‘false’数字数字转换字符串,比如:1.765转为’1.765’字符串无须转换对象(obj)先进行 ToPrimitive(obj, String)转换得到原始值,在进行ToString转换为字符串讲了这么多,是不是还不是很清晰,先来看看一个例子:

({} + {}) = ?
两个对象的值进行+运算符,肯定要先进行隐式转换为原始类型才能进行计算。
1、进行ToPrimitive转换,由于没有指定PreferredType类型,{}会使默认值为Number,进行ToPrimitive(input, Number)运算。
2、所以会执行valueOf方法,({}).valueOf(),返回的还是{}对象,不是原始值。
3、继续执行toString方法,({}).toString(),返回[object Object],是原始值。


再来一个指定类型的例子:

2 * {} = ?1、首先*运算符只能对number类型进行运算,故第一步就是对{}进行ToNumber类型转换。2、由于{}是对象类型,故先进行原始类型转换,ToPrimitive(input, Number)运算。3、所以会执行valueOf方法,({}).valueOf(),返回的还是{}对象,不是原始值。4、继续执行toString方法,({}).toString(),返回[object Object],是原始值。5、转换为原始值后再进行ToNumber运算,[object Object]就转换为NaN。故最终的结果为 2 * NaN = NaN


## 3、== 运算符隐式转换
== 运算符的规则规律性不是那么强,按照下面流程来执行,es5文档
比较运算 x==y, 其中 x 和 y 是值,返回 true 或者 false。这样的比较按如下方式进行:1、若 Type(x) 与 Type(y) 相同, 则

  1* 若 Type(x) 为 Undefined, 返回 true。
  2* 若 Type(x) 为 Null, 返回 true。
  3* 若 Type(x) 为 Number, 则

      (1)、若 x 为 NaN, 返回 false。
      (2)、若 y 为 NaN, 返回 false。
      (3)、若 x 与 y 为相等数值, 返回 true。
      (4)、若 x 为 +0 且 y 为 −0, 返回 true。
      (5)、若 x 为 −0 且 y 为 +0, 返回 true。
      (6)、返回 false。

  4* 若 Type(x) 为 String, 则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。 否则, 返回 false。
  5* 若 Type(x) 为 Boolean, 当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。
  6*  当 x 和 y 为引用同一对象时返回 true。否则,返回 false。

2、若 x 为 null 且 y 为 undefined, 返回 true。3、若 x 为 undefined 且 y 为 null, 返回 true。4、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。5、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。6、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。7、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。8、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。10、返回 false。


上面主要分为两类,x、y类型相同时,和类型不相同时。

类型相同时,没有类型转换,主要注意NaN不与任何值相等,包括它自己,即NaN !== NaN。

类型不相同时,

1、x,y 为null、undefined两者中一个 // 返回true

2、x、y为Number和String类型时,则转换为Number类型比较。

3、有Boolean类型时,Boolean转化为Number类型比较。

4、一个Object类型,一个String或Number类型,将Object类型进行原始转换后,按上面流程进行原始值比较。

### 3.1、== 例子解析

所以类型不相同时,可以会进行上面几条的比较,比如:
var a = { valueOf: function () { return1; }, toString: function () { return'123' }}true == a // true;首先,x与y类型不同,x为boolean类型,则进行ToNumber转换为1,为number类型。接着,x为number,y为object类型,对y进行原始转换,ToPrimitive(a, ?),没有指定转换类型,默认number类型。而后,ToPrimitive(a, Number)首先调用valueOf方法,返回1,得到原始类型1。最后 1 == 1, 返回true。

我们再看一段很复杂的比较,如下:
[] == !{}//1、! 运算符优先级高于==,故先进行!运算。2、!{}运算结果为false,结果变成 [] == false比较。3、根据上面第7条,等式右边y = ToNumber(false) = 0。结果变成 [] == 0。4、按照上面第9条,比较变成ToPrimitive([]) == 0。 按照上面规则进行原始值转换,[]会先调用valueOf函数,返回this。 不是原始值,继续调用toString方法,x = [].toString() = ''。 故结果为 '' == 0比较。5、根据上面第5条,等式左边x = ToNumber('') = 0。 所以结果变为: 0 == 0,返回true,比较结束。

最后我们看看文章开头说的那道题目:
const a = { i: 1, toString: function () { return a.i++; }}if (a == 1 && a == 2 && a == 3) { console.log('hello world!');}

1、当执行a == 1 >> a == 2 >> a == 3 时,会从左到右一步一步解析,首先 a == 1,会进行上面第9步转换。ToPrimitive(a, Number) == 1。

2、ToPrimitive(a, Number),按照上面原始类型转换规则,会先调用valueOf方法,a的valueOf方法继承自Object.prototype。返回a本身,而非原始类型,故会调用toString方法。

3、因为toString被重写,所以会调用重写的toString方法,故返回1,注意这里是i++,而不是++i,它会先返回i,在将i+1。故ToPrimitive(a, Number) = 1。也就是1 == 1,此时i = 1 + 1 = 2。

4、执行完a == 1返回true,会执行a == 2,同理,会调用ToPrimitive(a, Number),同上先调用valueOf方法,在调用toString方法,由于第一步,i = 2此时,ToPrimitive(a, Number) = 2, 也就是2 == 2, 此时i = 2 + 1。

5、同上可以推导 a == 3也返回true。故最终结果 a == 1 >> a == 2 >> a == 3返回true

其实了解了以上隐形转换的原理,你有没有发现这些隐式转换并没有想象中那么难。

转自:https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==>mid=2651227769>idx=1>sn=617160e64d2be13169b1b8f4506d8801>chksm=bd495ffd8a3ed6eb226d4ef193ff2ce3958d2d03d1f3047b635915f8215af40996c2f64d5c20>scene=38#wechat_redirect

mongoose连接数据库查询不到数据

2017-11-16 20:25 我说呢

本来兴高采烈的打算用下mongoose,没想到初体验印象这么不好

代码找了好几遍问题,都开始怀疑人生了。然后就各种搜资料,没想到网上也有人问这种问题,哈哈

然后就找到了这种问题的根源:

mongoose always add an s to the end of my collection name

WTF

知道根源后解决就好办了

1.把查询数据的collection加上一个s,然后在model里面别加s

2.当然还有第二种方法,就是强制让他使用某个collection

var dataSchema = new Schema({..}, { collection: 'data' })

问题解决了,不过还是禁不住想吐槽下。

location.replace not work in Android

2017-11-09 20:16 我说呢

location.replace()方法以给定的URL来替换当前的资源。 与assign() 方法 不同的是调用replace()方法后,当前页面不会保存到会话历史中(session History),这样用户点击回退按钮将不会再跳转到该页面。

这是MDN web docs对replace的解释,其实还挺权威的,不过表现不是那么权威啊,在android里面还是会保存到会话历史中(在IOS设备上OK)...

直接上解决方法吧:

function locationReplace (url) {
    if (history.replaceState) {
        history.replaceState(null, document.title, url);
        history.go(0);
    }
    else {
        location.replace(url);
    }
}

简单粗暴.

降龙十八掌

打完收工.

safari margin-bottom not work

2017-10-24 20:51 我说呢

来说下背景:当内容高度超出父容器高度时,内容的margin-bottom设置是无效的。

遇到的情况是这样的,在html,body设置height: 100%;的时候,如果页面超过一屏,最后的那个元素在设置margin-bottom失效了,在chrome、firefox可以

解决方法

1.设置padding-bottom可以

2.把html,body的height: 100%;去掉也可以。

最好是规避下超出所有父容器的内容。。。

原理其实我也不知道。。。。(尴尬不?????)

JavaScript 浮点数陷阱及解法

2017-10-19 19:51 奔跑的搬砖工

众所周知,JavaScript 浮点数运算时经常遇到会 0.000000001 和 0.999999999 这样奇怪的结果,如 0.1+0.2=0.30000000000000004、1-0.9=0.09999999999999998,很多人知道这是浮点数误差问题,但具体就说不清楚了。本文帮你理清这背后的原理以及解决方案,还会向你解释JS中的大数危机和四则运算中会遇到的坑。

浮点数的存储

首先要搞清楚 JavaScript 如何存储小数。和其它语言如 Java 和 Python 不同,JavaScript 中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。计算机组成原理中有过详细介绍,如果你不记得也没关系。

这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。

64位比特又可分为三个部分:

符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数

指数位E:中间的 11 位存储指数(exponent),用来表示次方数

尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零

img

实际数字就可以用以下公式来计算:

img

注意以上的公式遵循科学计数法的规范,在十进制是为0<M<10,到二进行就是0<M<2。也就是说整数部分只能是1,所以可以被舍去,只保留后面的小数部分。如 4.5 转换成二进制就是 100.1,科学计数法表示是 1.001*2^2,舍去1后 M = 001。E是一个无符号整数,因为长度是11位,取值范围是 0~2047。但是科学计数法中的指数是可以为负数的,所以再减去一个中间数 1023,[0,1022]表示为负,[1024,2047] 表示为正。如4.5 的指数E = 1025,尾数M为 001。

最终的公式变成:

img

所以 4.5 最终表示为(M=001、E=1025):

img

下面再以 0.1 例解释浮点误差的原因, 0.1 转成二进制表示为 0.0001100110011001100(1100循环),1.100110011001100x2^-4,所以 E=-4+1023=1019;M 舍去首位的1,得到 100110011...。最终就是:

img

转化成十进制后为 0.100000000000000005551115123126,因此就出现了浮点误差。

为什么 0.1+0.2=0.30000000000000004?

计算步骤为:

// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

// 转成十进制正好是 0.30000000000000004

为什么 x=0.1 能得到 0.1?

恭喜你到了看山不是山的境界。因为 mantissa 固定长度是 52 位,再加上省略的一位,最多可以表示的数是 2^53=9007199254740992,对应科学计数尾数是 9.007199254740992,这也是 JS 最多能表示的精度。它的长度是 16,所以可以使用 toPrecision(16) 来做精度运算,超过的精度会自动做凑整处理。于是就有:

0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零后正好为 0.1

// 但你看到的 `0.1` 实际上并不是 `0.1`。不信你可用更高的精度试试:
0.1.toPrecision(21) = 0.100000000000000005551

大数危机

可能你已经隐约感觉到了,如果整数大于 9007199254740992 会出现什么情况呢?

由于 E 最大值是 1023,所以最大可以表示的整数是 2^1024 - 1,这就是能表示的最大整数。但你并不能这样计算这个数字,因为从 2^1024 开始就变成了 Infinity

> Math.pow(2, 1023)
8.98846567431158e+307

> Math.pow(2, 1024)
Infinity

那么对于 (2^53, 2^63) 之间的数会出现什么情况呢?

(2^53, 2^54) 之间的数会两个选一个,只能精确表示偶数

(2^54, 2^55) 之间的数会四个选一个,只能精确表示4个倍数

... 依次跳过更多2的倍数

下面这张图能很好的表示 JavaScript 中浮点数和实数(Real Number)之间的对应关系。我们常用的 (-2^53, 2^53) 只是最中间非常小的一部分,越往两边越稀疏越不精确。

img

在淘宝早期的订单系统中把订单号当作数字处理,后来随意订单号暴增,已经超过了9007199254740992,最终的解法是把订单号改成字符串处理。

要想解决大数的问题你可以引用第三方库 bignumber.js,原理是把所有数字当作字符串,重新实现了计算逻辑,缺点是性能比原生的差很多。所以原生支持大数就很有必要了,现在 TC39 已经有一个 Stage 3 的提案 proposal bigint,大数问题有问彻底解决。

toPrecision vs toFixed

数据处理时,这两个函数很容易混淆。它们的共同点是把数字转成字符串供展示使用。注意在计算的中间过程不要使用,只用于最终结果。

不同点就需要注意一下:

toPrecision 是处理精度,精度是从左至右第一个不为0的数开始数起。

toFixed 是小数点后指定位数取整,从小数点开始数起。

两者都能对多余数字做凑整处理,也有些人用 toFixed 来做四舍五入,但一定要知道它是有 Bug 的。

如:1.005.toFixed(2) 返回的是 1.00 而不是 1.01。

原因: 1.005 实际对应的数字是 1.00499999999999989,在四舍五入时全部被舍去!

解法:使用专业的四舍五入函数 Math.round() 来处理。但 Math.round(1.005 * 100) / 100 还是不行,因为 1.005 * 100 = 100.49999999999999。还需要把乘法和除法精度误差都解决后再使用 Math.round。可以使用后面介绍的 number-precision#round 方法来解决。

解决方案

回到最关心的问题:如何解决浮点误差。首先,理论上用有限的空间来存储无限的小数是不可能保证精确的,但我们可以处理一下得到我们期望的结果。

数据展示类

当你拿到 1.4000000000000001 这样的数据要展示时,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示,如下:

parseFloat(1.4000000000000001.toPrecision(12)) === 1.4  // True

封装成方法就是:

function strip(num, precision = 12) {
  return +parseFloat(num.toPrecision(precision));
}

为什么选择 12 做为默认精度?这是一个经验的选择,一般选12就能解决掉大部分0001和0009问题,而且大部分情况下也够用了,如果你需要更精确可以调高。

数据运算类

对于运算类操作,如 +-*/,就不能使用 toPrecision 了。正确的做法是把小数转成整数后再运算。以加法为例:

/**
 * 精确加法
 */
function add(num1, num2) {
  const num1Digits = (num1.toString().split('.')[1] || '').length;
  const num2Digits = (num2.toString().split('.')[1] || '').length;
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
  return (num1 * baseNum + num2 * baseNum) / baseNum;
}

以上方法能适用于大部分场景。遇到科学计数法如 2.3e+1(当数字精度大于21时,数字会强制转为科学计数法形式显示)时还需要特别处理一下。

能读到这里,说明你非常有耐心,那我就放个福利吧。遇到浮点数误差问题时可以直接使用https://github.com/dt-fe/number-precision

完美支持浮点数的加减乘除、四舍五入等运算。非常小只有1K,远小于绝大多数同类库(如Math.js、BigDecimal.js),100%测试全覆盖,代码可读性强,不妨在你的应用里用起来!

转载自:https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651227117&idx=1&sn=74dc9e59b1f0006ff16defc299b02a9c&chksm=bd495a698a3ed37f31932c421af1976224dd38af466a8d3bbb9d2480a7194108b3468ea4e1a9&scene=38#%23