3. Symbol类型在实际开发中的应用、可手动实现一个简单的 Symbo
6. 理解值类型和引用类型
8. 至少可以说出三种判断 JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
9. 可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用
1. JavaScript
规定了几种语言类型(推荐阅读 https://www.cnblogs.com/onepixel/p/5140944.html)
答:JavaScript有7大基本类型。(6种原始类型,1种引用类型) https://www.cnblogs.com/memphis-f/p/11913756.html
1)、Undefined
undefined表示未定义,它的值只有一个:undefined。当声明的变量未初始化时,变量的默认值是undefined
任何变量赋值前都是undefined类型,值为undefined而不是null,undefined是一个变量而不是一个关键字。
我们一般不会把变量赋值为undefined,这样可以保证所有值为undefined的变量,都是从未赋值的自然变量。
2)、Null
只有一个值就是null,表示空值,是关键字。
null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象
3)、Boolean
Boolean(true) // true Boolean(false) // false Boolean('Hello Wolrd') // true Boolean() // false Boolean('') // false Boolean(' ') // true (里面有空格) Boolean(1) // true Boolean(0) // false Boolean(NaN) // false Boolean({}) // true Boolean([]) // true Boolean(null) // false Boolean(undefined) // false
把其他类型转换为Boolean类型有三种方式:
1)Boolean()
2)! 或者 !!,取反,先转为Boolean然后再取反
3)条件判断
4)、String
字符串是存储字符的变量。
在JS中的字符串需要用引号引起来(英文单引号或者双引号)
String对象方法:
1)indexOf()
该方法可返回某个指定字符串值在字符串中首次出现的位置。并返回第一次出现的位置。如果要检索的字符串值没有出现,则返回-1
2)lastIndexOf()
该方法可返回某个指定字符串值在字符串中最后出现的位置,在一个字符串中的指定位置从后向前检索。如果要检索的字符串值没有出现,则返回-1
3)concat()
该方法用于连接两个或多个字符串。使用 + 号运算符来进行字符串的连接运算通常会更简便些。
4)slice()
该方法可提取字符串的某个部分,并以返回被提取的部分
5)toLowerCase()
该方法用于将字符串转换为小写
6)toUpperCase()
该方法用于将字符串转换为大写
5)、Number
Number是与数字值对应的引用类型
常用方法:
1)toFixed()
方法会按照指定的小数位返回数值的字符串表示
2)toExponential()
该方法返回以指数表示法(也称 e 表示法)表示的数值的字符串形式
3)toPrecision()
方法可能会返回固定大小(fixed)格式,也可能返回指数(exponential)格式;具体规则是看哪种格式最合适。
这个方法接收一个参数,即表示数值的所有数字的位数(不包括指数部分)。
var num = 10; alert(num.toFixed(2)); //"10.00" var num = 10; alert(num.toExponential(1)); //"1.0e+1" var num = 99; alert(num.toPrecision(1)); //"1e+2" alert(num.toPrecision(2)); //"99" alert(num.toPrecision(3)); //"99.0"
6)、Symbol
表示独一无二的值,它是一切非字符串的对象key的集合。 Symbol 值通过Symbol函数生成。
这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。
凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,但是即使描述相同,Symbol值也不相等。
let s1 = Symbol('foo'); let s2 = Symbol('foo'); s1 === s2 // false 一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。例如,我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为: var o = new Object o[Symbol.iterator] = function() { var v = 0 return { next: function() { return { value: v++, done: v > 10 } } } }; for(var v of o) console.log(v); // 0 1 2 3 ... 9
7)、Object
对象是某个特定引用类型的实例。在ECMAScript中,object类型是所有它的实例的基础。换句话说,Object类型所具有的任何属性和方法也同样存在于更具体的对象中。
Object的每个实例都具有下列的属性和方法:
- [x] constructor: 构造函数
-
[x] hasOwnProperty(propertyName)
用于检查给定的属性在当前对象实例(而不是实例的原型)中是否存在。 -
[x] isPrototypeOf(Object):
用于检查其原型链的对象是否存在于指定对象的实例中,是则返回true,否则返回false。
例如:var a = {} function Person() {} var p1 = new Person() // 继承自原来的原型,但现在已经无法访问 var Person.prototype = a var p2 = new Person() // 继承a console.log(a.isPrototypeOf(p1)) // false a是不是p1的原型 console.log(a.isPrototypeOf(p2)) // true a是不是p2的原型 console.log(Object.prototype.isPrototypeOf(p1)) // true console.log(Object.prototype.isPrototypeOf(p2)) // true
- [x] propertyIsEnumerable(propertyName)
用于检查给定的属性是否可以用 for-in 语句进行枚举。 - [x] toLocaleString()
返回对象的字符串表示,该字符串与执行环境的地区对应。 - [x] toString()
返回对象的字符串表示。 - [x] valueOf()
返回对象的字符串、数值、布尔值表示。通常与toString()方法的返回值相同。
创建Object实例的方法:
1、第一种是使用new操作符后跟Object构造函数。
var person=new Object(); person.name="Nicholas"; person.age=29;
2、使用对象字面量。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。
var person={ name:"Nicholas", age:29 };
2. JavaScript
对象的底层数据结构是什么
参考文章:再谈JS 对象数据结构底层实现原理
3. Symbol
类型在实际开发中的应用、可手动实现一个简单的 Symbol(参考文档)
答:Symbol
是由ES6规范引入的一项新特性,它的功能类似于一种标识唯一性的ID。每个Symbol实例都是唯一的。
因此,当你比较两个Symbol实例的时候,将总会返回false
:
let s1 = Symbol() let s2 = Symbol('another symbol') let s3 = Symbol('another symbol') s1 === s2 // false s2 === s3 // false
应用场景1:使用symbol来作为对象属性名(key)
// ① 通常我们定义或访问对象的属性时都是使用字符串 let obj = { abc: 123, "hello": "world" } obj["abc"] // 123 obj["hello"] // 'world' // ② 现在symbol同样可以用于对象属性的定义和访问 const PROP_NAME = Symbol() const PROP_AGE = Symbol() let obj = { [PROP_NAME]: "一斤代码" } obj[PROP_AGE] = 18 obj[PROP_NAME] // '一斤代码' obj[PROP_AGE] // 18 // ③ Symbol类型的key是不能通过Object.keys()或者for...in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。如下: let obj = { [Symbol('name')]: '一斤代码', age: 18, title: 'Engineer' } Object.keys(obj) // ['age', 'title'] for (let p in obj) { console.log(p) // 分别会输出:'age' 和 'title' } Object.getOwnPropertyNames(obj) // ['age', 'title']
// ④ 也正因为这样一个特性,当使用JSON.stringify()将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外: JSON.stringify(obj) // {"age":18,"title":"Engineer"}
// ⑤ 我们可以利用这一特点来更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。 // ⑥ 获取以symbol方式定义对象属性的API // 使用Object的API Object.getOwnPropertySymbols(obj) // [Symbol(name)] // 使用新增的反射API Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']
应用场景2:使用symbol来代替常量
// 我们经常定义一组常量来代表一种业务逻辑下的几个不同类型,我们通常希望这几个常量之间是唯一的关系,为了保证这一点,我们需要为常量赋一个唯一的值(比如这里的'AUDIO'、'VIDEO'、 'IMAGE'),
常量少的时候还算好,但是常量一多,你可能还得花点脑子好好为他们取个好点的名字。如下: const TYPE_AUDIO = 'AUDIO' const TYPE_VIDEO = 'VIDEO' const TYPE_IMAGE = 'IMAGE' function handleFileResource(resource) { switch(resource.type) { case TYPE_AUDIO: playAudio(resource) break case TYPE_VIDEO: playVideo(resource) break case TYPE_IMAGE: previewImage(resource) break default: throw new Error('Unknown type of resource') } } // 现在有了Symbol,我们大可不必这么麻烦了,这样定义,直接就保证了三个常量的值是唯一的了: const TYPE_AUDIO = Symbol() const TYPE_VIDEO = Symbol() const TYPE_IMAGE = Symbol()
应用场景3:使用Symbol定义类的私有属性/方法
// 在JavaScript中,是没有如Java等面向对象语言的访问控制关键字private的,类上所有定义的属性或方法都是可公开访问的。因此这对我们进行API的设计时造成了一些困扰。 而有了Symbol以及模块化机制,类的私有属性和方法才变成可能。例如: 在文件 a.js中: const PASSWORD = Symbol( class Login { constructor(username, password) { this.username = username this[PASSWORD] = password } checkPassword(pwd) { return this[PASSWORD] === pwd } } export default Login 在文件 b.js 中: import Login from './a' const login = new Login('admin', '123456') login.checkPassword('123456') // true login.PASSWORD // oh!no! login[PASSWORD] // oh!no! login["PASSWORD"] // oh!no! // 由于Symbol常量PASSWORD被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也不可能再创建一个一模一样的Symbol出来(因为Symbol是唯一的),
// 因此这个PASSWORD的Symbol只能被限制在a.js内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。
4. JavaScript
中的变量在内存中的具体存储形式(参考文档)
答:JavaScript中的变量分为基本类型和引用类型
基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问
引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用
- 引用类型的复制,同样为新的变量b分配一个新的值,保存在栈内存中,不同的是,这个值仅仅是引用类型的一个地址指针
- 他们两个指向同一个值,也就是地址指针相同,在堆内存中访问到的具体对象实际上是同一个
- 因此改变b.x时,a.x也发生了变化,这就是引用类型的特性
答:基本类型对应的内置对象:String()、Number()、Boolean()、RegExp()、Date()、Error()、Array()、
Function()、Object()、symbol();类似于对象的构造函数
1、这些内置函数构造的变量都是封装了基本类型值的对象如:
Var a=new String(‘abb’); //typeof(a)=object
除了利用Function()构造的变量通过typeof输出为function外其他均为object
2、为了知道构造的变量的真实类型可以利用:
Object.prototype.toString.call([1,2,3]);//”[object,array]”,后面的一个值即为传入参数的类型
3、如果有常量形式(即利用基本数据类型)赋值给变量就不要用该方式来定义变量
一、装箱
所谓装箱,就是把基本类型转变为对应的对象。装箱分为隐式和显式:
隐式
// 每当读取一个基本类型的值时,后台会创建一个该基本类型所对应的对象。在这个基本类型上调用方法,其实是在这个基本类型对象上调用方法。这个基本类型的对象是临时的,它只存在于方法调用那一行代码执行的瞬间,执行方法后立刻被销毁。具体到代码如下: num.toFixed(2); // '123.00' // 上方代码在后台的真正步骤为 var c = new Number(123); c.toFixed(2); c = null; // 当我们访问 num 时,要从内存中读取这个数字的值,此时访问过程处于读取模式。在读取模式中,后台进行了三步处理: /** 1、创建一个 Number 类型的实例。 2、在实例上调用方法。 3、销毁实例。 **/
显式
// 通过内置对象 Boolean Object String等可以对基本类型进行显式装箱 var obj = new String('123');
二、拆箱
拆箱与装箱相反,把对象转变为基本类型的值。
// 拆箱过程内部调用了抽象操作ToPrimitive: // 该操作接受两个参数,第一个参数是要转变的对象,第二个参数 PreferredType 是对象被期待转成的类型。 ToPrimitive('要转变的对象', PreferredType) { // 操作方法 ........ } // 第二个参数不是必须的,默认该参数为 number,即对象被期待转为数字类型。有些操作如 String(obj) 会传入 PreferredType 参数。有些操作如 obj + " " 不会传入 PreferredType。 // 具体转换过程是这样的: // ① 默认情况下,ToPrimitive 先检查对象是否有 valueOf 方法,如果有则再检查 valueOf 方法是否有基本类型的返回值; // ② 如果没有 valueOf 方法或 valueOf 方法没有返回值,则调用 toString 方法; // ③ 如果 toString 方法也没有返回值,产生 TypeError 错误。 // PreferredType 影响 valueOf 与 toString 的调用顺序。如果 PreferrenType 的值为 string。则先调用 toString ,再调用 valueOf。 // 具体测试代码如下: var obj = { valueOf : () => {console.log("valueOf"); return []}, toString : () => {console.log("toString"); return []} } String(obj) // toString // valueOf // Uncaught TypeError: Cannot convert object to primitive value obj+' ' //valueOf //toString // Uncaught TypeError: Cannot convert object to primitive value Number(obj) //valueOf //toString // Uncaught TypeError: Cannot convert object to primitive value
答:JavaScript中的变量类型:
(1) 值类型(基本类型):字符串(string)、数值(number)、布尔值(boolean)、undefined、null (这5种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值)
(ECMAScript 2016新增了一种基本数据类型:symbol http://es6.ruanyifeng.com/#docs/symbol )
(2) 引用类型:对象(Object)、数组(Array)、函数(Function)
值类型和引用类型的区别
(1) 值类型:
1、占用空间固定,保存在栈中
(当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。
因此,所有在方法中定义的变量都是放在栈内存中的;
栈中存储的是基础变量以及一些对象的引用变;
基础变量的值是存储在栈中;
而引用变量存储在栈中的是指向堆中的数组或者对象的地址。
这就是为何修改引用类型总会影响到其他指向这个地址的引用变量。)
2、保存与复制的是值本身
3、使用typeof检测数据的类型
4、基本类型数据是值类型
(2) 引用类型:
1、占用空间不固定,保存在堆中
(当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。
堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),
则这个对象依然不会被销毁
,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。)
2、保存与复制的是指向对象的一个指针
3、使用instanceof检测数据类型
4、使用new()方法构造出的对象是引用型
答:null:Null类型,代表 “空值”,代表一个空对象指针,使用typeof运算得到 “object” ,所以可以认为它是一个特殊的对象值。表示"没有对象",即该处不应该有值。用法:
Object.getPrototypeOf(Object.prototype)
// null
(1) 作为函数的参数,表示该函数的参数不是对象。 (2) 作为对象原型链的终点。
undefined:Undefined类型只有一个值,即undefined。表示"缺少值",就是此处应该有一个值,但是还没有定义。用法:
(1)变量被声明了,但没有赋值时,就等于undefined。(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。(3)对象没有赋值的属性,该属性的值为undefined。(4)函数没有返回值时,默认返回undefined。
知识了解:
undefined与null的区别,这与JavaScript的历史有关。1995年JavaScript诞生时,最初像Java一样,只设置了null作为表示"无"的值。
根据C语言的传统,null被设计成可以自动转为0。
Number(null)
// 0
5 + null
// 5
但是,JavaScript的设计者Brendan Eich,觉得这样做还不够,有两个原因。
首先,null像在Java里一样,被当成一个对象。但是,JavaScript的数据类型分成原始类型(primitive)和合成类型(complex)两大类,Brendan Eich觉得表示"无"的值最好不是对象。
其次,JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。Brendan Eich觉得,如果null自动转为0,很不容易发现错误。
因此,Brendan Eich又设计了一个undefined。
8. 至少可以说出三种判断 JavaScript
数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
答:判断数据类型的方法一般可以通过:typeof、instanceof、constructor、toString四种常用方法:
1、typeof (可以对基本类型做出准确的判断,但对于引用类型,用它就有点力不从心了)
typeof返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、object、undefined、function等6种数据类型。
typeof 可以对JS基本数据类型做出准确的判断(除了null),而对于引用类型返回的基本上都是object,。
其实返回object也没有错,因为所有对象的原型链最终都指向了Object,Object是所有对象的`祖宗`。 但当我们需要知道某个对象的具体类型时,typeof 就显得有些力不从心了。
注意:typeof null会返回object,因为特殊值null被认为是一个空的对象引用
2、instanceof
判断对象和构造函数在原型链上是否有关系。有关系返回真,否则返回假。
function Aaa(){} var a1 = new Aaa(); //alert( a1 instanceof Aaa); //true 判断a1和Aaa是否在同一个原型链上,是的话返回真,否则返回假 var arr = []; alert( arr instanceof Aaa);//false
看一下下面的判断: var str = 'hello'; alert(str instanceof String);//false var bool = true; alert(bool instanceof Boolean);//false var num = 123; alert(num instanceof Number);//false var nul = null; alert(nul instanceof Object);//false var und = undefined; alert(und instanceof Object);//false var oDate = new Date(); alert(oDate instanceof Date);//true var json = {}; alert(json instanceof Object);//true var arr = []; alert(arr instanceof Array);//true var reg = /a/; alert(reg instanceof RegExp);//true var fun = function(){}; alert(fun instanceof Function);//true var error = new Error(); alert(error instanceof Error);//true 从上面的运行结果我们可以看到,基本数据类型是没有检测出他们的类型,但是我们使用下面的方式创建num、str、boolean,是可以检测出类型的: var num = new Number(123); alert(num instanceof Number);//true var str = new String('abcdef'); alert(str instanceof String);//true var boolean = new Boolean(true); alert(boolean instanceof Boolean;//true
3、constructor:查看对象对应的构造函数
constructor在其对应的原型下面,是自动生成的。在我们写一个构造函数的时候,程序会自动添加:构造函数名.prototype.constructor = 构造函数名
function Aaa(){} //Aaa.prototype.constructor = Aaa; //每一个函数都会有的,都是自动生成的
判断数据类型的方法
var str = 'hello'; alert(str.constructor == String);//true var bool = true; alert(bool.constructor == Boolean);//true var num = 123; alert(num.constructor ==Number);//true // var nul = null; // alert(nul.constructor == Object);//报错 //var und = undefined; //alert(und.constructor == Object);//报错 var oDate = new Date(); alert(oDate.constructor == Date);//true var json = {}; alert(json.constructor == Object);//true var arr = []; alert(arr.constructor == Array);//true var reg = /a/; alert(reg.constructor == RegExp);//true var fun = function(){}; alert(fun.constructor ==Function);//true var error = new Error(); alert(error.constructor == Error);//true // 从上面的测试中我们可以看到,undefined和null是不能够判断出类型的,并且会报错。因为null和undefined是无效的对象,因此是不会有constructor存在的 // 同时我们也需要注意到的是:使用constructor是不保险的,因为constructor属性是可以被修改的,会导致检测出的结果不正确 function Aaa(){} Aaa.prototype.constructor = Aaa;//程序可以自动添加,当我们写个构造函数的时候,程序会自动添加这句代码 function BBB(){} Aaa.prototype.constructor = BBB;//此时我们就修改了Aaa构造函数的指向问题 alert(Aaa.construtor==Aaa);//false // 可以看出,constructor并没有正确检测出正确的构造函数
4、Object.prototype.toString()
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
判断类型example:
Object.prototype.toString.call('') ; // [object String] Object.prototype.toString.call(1) ; // [object Number] Object.prototype.toString.call(true) ; // [object Boolean] Object.prototype.toString.call(Symbol()); //[object Symbol] Object.prototype.toString.call(undefined) ; // [object Undefined] Object.prototype.toString.call(null) ; // [object Null] Object.prototype.toString.call(new Function()) ; // [object Function] Object.prototype.toString.call(new Date()) ; // [object Date] Object.prototype.toString.call([]) ; // [object Array] Object.prototype.toString.call(new RegExp()) ; // [object RegExp] Object.prototype.toString.call(new Error()) ; // [object Error] Object.prototype.toString.call(document) ; // [object HTMLDocument] Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
9. 可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用
答:1、运算符转换
-,*,/,%会将操作数转换为数字去计算,但+不一样,两边纯数字会按数字相加,纯字符串会拼接,但数字和字符串也会将字符串和数字拼接起来。
console.log("1 - '2'"); console.log(1 - '2'); //-1 console.log("1 * '2'"); console.log(1 * '2'); //2 console.log("6 / '4'"); console.log(6 / '4'); //1.5 console.log("6 % '4'"); console.log(6 % '4'); //2
console.log("6 + 4"); console.log(6 + 4); //10 console.log("6 + '4'"); console.log(6 + '4'); //64 console.log("'6' + '4'"); console.log('6' + '4'); //64 console.log("typeof'6' + '4'"); console.log(typeof('6' + '4')); //string
2、双等号转换
两边会转换为同一类型再进行比较。双等号两边只要有以便是NaN,便返回false,且他自身不相等
console.log("NaN == 1"); console.log(NaN == 1); //false console.log("NaN == NaN"); console.log(NaN == NaN);//false console.log("undefined == NaN"); console.log(undefined == NaN);//false
布尔值会转换为数字,false转换为0,true转换为1
console.log('0 == false'); console.log(0 == false); console.log('1 == true'); console.log(1 == true);
对象的转换
var a = []; var b = []; var c = {}; var d = {}; console.log("[] == []"); console.log(a == b); // false console.log("[] == {}"); console.log(a == c); // false console.log("{} == {}"); console.log(d == c); // false console.log("[] == ![]"); console.log(a == !b); // true // 对于前三个的原理是一样的,当两个值都是对象 (引用值) 时, 比较的是两个引用值在内存中是否是同一个对象. 因为此 [] 非彼 [], 虽然同为空数组, 确是两个互不相关的空数组, 所以为false。 // 而最后一个是因为右边空数组会转化为true,取反变为false,false变为0;左边空数组变为空字符串再变为0,0==0就为true。
3、· 点号操作符
数字、字符串等直接做·操作调用方法时,隐式地将类型转换成对象。
4、if()语句
括号里的表达式部分会被隐式转换为布尔类型进行判别
10.出现小数精度丢失的原因, JavaScript
可以存储的最大数字、最大安全数字, JavaScript
处理大数字的方法、避免精度丢失的方法
答:精度丢失:
因为有些小数在计算机使用二进制方式表示时无法准确的表示出来,类似于十进制中的1/3一样,无理数,无限循环.
可是计算机存储小数的类型不管是float还是double都是有位数限制的,所以他们存储的只是一个近似值,这就导致了精度丢失.
解决办法:
对于整数,前端出现问题的几率可能比较低,毕竟很少有业务需要需要用到超大整数,只要运算结果不超过 Math.pow(2, 53) 就不会丢失精度。
对于小数,前端出现问题的几率还是很多的,尤其在一些电商网站涉及到金额等数据。解决方式:把小数放到位整数(乘倍数),再缩小回原来倍数(除倍数)
// 0.1 + 0.2 (0.1*10 + 0.2*10) / 10 == 0.3 // true
一位网友写的方法,对小数的加减乘除丢失精度做了屏蔽,换算后的整数不能超过 9007199254740992:
(大整数的精度丢失和浮点数本质上是一样的,尾数位最大是 52 位,因此 JS 中能精准表示的最大整数是 Math.pow(2, 53),十进制即 9007199254740992。)
/** * floatObj 包含加减乘除四个方法,能确保浮点数运算不丢失精度 * * 我们知道计算机编程语言里浮点数计算会存在精度丢失问题(或称舍入误差),其根本原因是二进制和实现位数限制有些数无法有限表示 * 以下是十进制小数对应的二进制表示 * 0.1 >> 0.0001 1001 1001 1001…(1001无限循环) * 0.2 >> 0.0011 0011 0011 0011…(0011无限循环) * 计算机里每种数据类型的存储是一个有限宽度,比如 JavaScript 使用 64 位存储数字类型,因此超出的会舍去。舍去的部分就是精度丢失的部分。 * * ** method ** * add / subtract / multiply /divide * * ** explame ** * 0.1 + 0.2 == 0.30000000000000004 (多了 0.00000000000004) * 0.2 + 0.4 == 0.6000000000000001 (多了 0.0000000000001) * 19.9 * 100 == 1989.9999999999998 (少了 0.0000000000002) * * floatObj.add(0.1, 0.2) >> 0.3 * floatObj.multiply(19.9, 100) >> 1990 * */ var floatObj = function() { /* * 判断obj是否为一个整数 */ function isInteger(obj) { return Math.floor(obj) === obj } /* * 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100 * @param floatNum {number} 小数 * @return {object} * {times:100, num: 314} */ function toInteger(floatNum) { var ret = {times: 1, num: 0} var isNegative = floatNum < 0 if (isInteger(floatNum)) { ret.num = floatNum return ret } var strfi = floatNum + '' var dotPos = strfi.indexOf('.') var len = strfi.substr(dotPos+1).length var times = Math.pow(10, len) var intNum = parseInt(Math.abs(floatNum) * times + 0.5, 10) ret.times = times if (isNegative) { intNum = -intNum } ret.num = intNum return ret } /* * 核心方法,实现加减乘除运算,确保不丢失精度 * 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除) * * @param a {number} 运算数1 * @param b {number} 运算数2 * @param digits {number} 精度,保留的小数点数,比如 2, 即保留为两位小数 * @param op {string} 运算类型,有加减乘除(add/subtract/multiply/divide) * */ function operation(a, b, digits, op) { var o1 = toInteger(a) var o2 = toInteger(b) var n1 = o1.num var n2 = o2.num var t1 = o1.times var t2 = o2.times var max = t1 > t2 ? t1 : t2 var result = null switch (op) { case 'add': if (t1 === t2) { // 两个小数位数相同 result = n1 + n2 } else if (t1 > t2) { // o1 小数位 大于 o2 result = n1 + n2 * (t1 / t2) } else { // o1 小数位 小于 o2 result = n1 * (t2 / t1) + n2 } return result / max case 'subtract': if (t1 === t2) { result = n1 - n2 } else if (t1 > t2) { result = n1 - n2 * (t1 / t2) } else { result = n1 * (t2 / t1) - n2 } return result / max case 'multiply': result = (n1 * n2) / (t1 * t2) return result case 'divide': result = (n1 / n2) * (t2 / t1) return result } } // 加减乘除的四个接口 function add(a, b, digits) { return operation(a, b, digits, 'add') } function subtract(a, b, digits) { return operation(a, b, digits, 'subtract') } function multiply(a, b, digits) { return operation(a, b, digits, 'multiply') } function divide(a, b, digits) { return operation(a, b, digits, 'divide') } // exports return { add: add, subtract: subtract, multiply: multiply, divide: divide } }();