变量和类型

作者: 旭哥_ | 来源:发表于2019-10-05 13:32 被阅读0次

    前端工程师吃饭的家伙,深度、广度一样都不能差。

    一、JavaScript 规定了几种语言类型

    7 种基本数据类型:BigInt、Symbol、Undefined、Null、Boolean、Number和String

    1 种复杂数据类型:Object

    二、JavaScript 对象的底层数据结构是什么

    通过 V8 的源码尝试分析 Object 的实现:V8 里面所有的数据类型的根父类都是 Object,Object 派生 HeapObject,提供存储基本功能,往下的 JSReceiver 用于原型查找,再往下的 JSObject 就是 JS 里面的 Object,Array/Function/Date 等继承于 JSObject。左边的 FixedArray 是实际存储数据的地方。



    在创建一个 JSObject 之前,会先把读到的 Object 的文本属性序列化成 constant_properties,如下的 data:

    var data = {
      name: 'yin',
      age: 18,
      '-school-': 'high school'
    }
    

    会被序列成:

    ../../v8/src/runtime/runtime-literals.cc 72 constant_properties:

    0xdf9ed2aed19: [FixedArray]
    
    – length: 6
    
      [0]: 0x1b5ec69833d1 <String[4]: name>
    
      [1]: 0xdf9ed2aec51 <String[3]: yin>
    
      [2]: 0xdf9ed2aec71 <String[3]: age>
    
      [3]: 18
    
      [4]: 0xdf9ed2aec91 <String[8]: -school->
    
      [5]: 0xdf9ed2aecb1 <String[11]: high school>
    

    它是一个 FixedArray,一共有 6 个元素,由于 data 总共是有 3 个属性,每个属性有一个 key 和一个 value,所以 Array 就有 6 个。第一个元素是第一个 key,第二个元素是第一个 value,第三个元素是第二个 key,第四个元素是第二个 key,依次类推。

    Object 提供了一个 Print()的函数,把它用来打印对象的信息非常有帮助。上面的输出有两种类型的数据,一种是 String 类型,第二种是整型类型的。
    FixedArray 是 V8 实现的一个类似于数组的类,它表示一段连续的内存。

    参考自:https://www.rrfed.com/2017/04/04/chrome-object/

    三、Symbol 类型在实际开发中的应用、可手动实现一个简单的 Symbol

    应用场景 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 后,在对该对象进行 key 的枚举时,会有什么不同?在实际应用中,我们经常会需要使用 Object.keys()或者 for...in 来枚举对象的属性名,那在这方面,Symbol 类型的 key 表现的会有什么不同之处呢?来看以下示例代码:

    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']
    

    由上代码可知,Symbol 类型的 key 是不能通过 Object.keys()或者 for...in 来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol 来定义。

    也正因为这样一个特性,当使用 JSON.stringify()将对象转换成 JSON 字符串的时候,Symbol 属性也会被排除在输出内容之外:

    JSON.stringify(obj) // {"age":18,"title":"Engineer"}
    

    我们可以利用这一特点来更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。

    然而,这样的话,我们就没办法获取以 Symbol 方式定义的对象属性了么?非也。还是会有一些专门针对 Symbol 的 API,比如:

    // 使用Object的API
    Object.getOwnPropertySymbols(obj) // [Symbol(name)]
    
    // 使用新增的反射API
    Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']
    

    应用场景 2:使用 Symbol 来替代常量

    先来看一下下面的代码,是不是在你的代码里经常会出现?

    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')
      }
    }
    

    如上面的代码中那样,我们经常定义一组常量来代表一种业务逻辑下的几个不同类型,我们通常希望这几个常量之间是唯一的关系,为了保证这一点,我们需要为常量赋一个唯一的值(比如这里的'AUDIO'、'VIDEO'、 'IMAGE'),常量少的时候还算好,但是常量一多,你可能还得花点脑子好好为他们取个好点的名字。

    现在有了 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 内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。

    手动实现 Symbol:

    (function() {
        var root = this;
    
        var generateName = (function(){
            var postfix = 0;
            return function(descString){
                postfix++;
                return '@@' + descString + '_' + postfix
            }
        })()
    
        var SymbolPolyfill = function Symbol(description) {
    
            if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');
    
            var descString = description === undefined ? undefined : String(description)
    
            var symbol = Object.create({
                toString: function() {
                    return this.__Name__;
                },
                valueOf: function() {
                    return this;
                }
            })
    
            Object.defineProperties(symbol, {
                '__Description__': {
                    value: descString,
                    writable: false,
                    enumerable: false,
                    configurable: false
                },
                '__Name__': {
                    value: generateName(descString),
                    writable: false,
                    enumerable: false,
                    configurable: false
                }
            });
    
            return symbol;
        }
    
        var forMap = {};
    
        Object.defineProperties(SymbolPolyfill, {
            'for': {
                value: function(description) {
                    var descString = description === undefined ? undefined : String(description)
                    return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
                },
                writable: true,
                enumerable: false,
                configurable: true
            },
            'keyFor': {
                value: function(symbol) {
                    for (var key in forMap) {
                        if (forMap[key] === symbol) return key;
                    }
                },
                writable: true,
                enumerable: false,
                configurable: true
            }
        });
    
        root.SymbolPolyfill = SymbolPolyfill;
    
    
    

    四、JavaScript 中的变量在内存中的具体存储形式

    栈内存和堆内存

    JavaScript 中的变量分为基本类型和引用类型
    基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问

    引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用

    结合代码与图来理解

    let a1 = 0 // 栈内存
    let a2 = 'this is string' // 栈内存
    let a3 = null // 栈内存
    let b = { x: 10 } // 变量b存在于栈中,{ x: 10 }作为对象存在于堆中
    let c = [1, 2, 3] // 变量c存在于栈中,[1, 2, 3]作为对象存在于堆中
    

    当我们要访问堆内存中的引用数据类型时

    1. 从栈中获取该对象的地址引用
    2. 再从堆内存中取得我们需要的数据

    基本类型发生复制行为

    let a = 20
    let b = a
    b = 30
    console.log(a) // 20
    

    结合下面图进行理解:

    在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值,最后这些变量都是相互独立互不影响的

    引用类型发生复制行为

    let a = { x: 10, y: 20 }
    let b = a
    b.x = 5
    console.log(a.x) // 5
    
    1. 引用类型的复制,同样为新的变量 b 分配一个新的值,保存在栈内存中,不同的是,这个值仅仅是引用类型的一个地址指针
    2. 他们两个指向同一个值,也就是地址指针相同,在堆内存中访问到的具体对象实际上是同一个
    3. 因此改变 b.x 时,a.x 也发生了变化,这就是引用类型的特性

    结合下图理解

    总结

    五、基本类型对应的内置对象,以及他们之间的装箱拆箱操作

    JS 中的内置函数(对象)

    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、如果有常量形式(即利用基本数据类型)赋值给变量就不要用该方式来定义变量

    装箱

    就是把基本类型转变为对应的对象。装箱分为隐式和显示

    • 隐式装箱: 每当读取一个基本类型的值时,后台会创建一个该基本类型所对应的对象。在这个基本类型上调用方法,其实是在这个基本类型对象上调用方法。这个基本类型的对象是临时的,它只存在于方法调用那一行代码执行的瞬间,执行方法后立刻被销毁。
    let num = 123
    num.toFixed(2) // '123.00'//上方代码在后台的真正步骤为
    var c = new Number(123)
    c.toFixed(2)
    c = null
    

    (1)创建一个 Number 类型的实例。

    (2)在实例上调用方法。

    (3)销毁实例。

    • 显式装箱: 通过内置对象 Boolean、Object、String 等可以对基本类型进行显示装箱。
    var obj = new String('123')
    

    拆箱

    拆箱与装箱相反,把对象转变为基本类型的值。拆箱过程内部调用了抽象操作 ToPrimitive 。该操作接受两个参数,第一个参数是要转变的对象,第二个参数 PreferredType 是对象被期待转成的类型。第二个参数不是必须的,默认该参数为 number,即对象被期待转为数字类型

    • Number 转化为对象

      1.先调用对象自身的 valueOf 方法。如果返回原始类型的值,则直接对该值使用 Number 函数,返回结果。

      2.如果 valueOf 返回的还是对象,继续调用对象自身的 toString 方法。如果 toString 返回原始类型的值,则对该值使用 Number 函数,返回结果。

      3.如果 toString 返回的还是对象,报错。

    Number([1]); //1
    转换演变:
    [1].valueOf(); // [1];
    [1].toString(); // '1';Number('1'); //1
    
    • String 转化为对象

      1.先调用对象自身的 toString 方法。如果返回原始类型的值,则对该值使用 String 函数,返回结果。

      2.如果 toString 返回的是对象,继续调用 valueOf 方法。如果 valueOf 返回原始类型的值,则对该值使用 String 函数,返回结果。

      3.如果 valueOf 返回的还是对象,报错。

    String([1,2]) //"1,2"
    转化演变:
    [1,2].toString();  //"1,2"
    String("1,2");  //"1,2"
    
    • Boolean 转化对象

      Boolean 转换对象很特别,除了以下六个值转换为 false,其他都为 true

    undefined  null  false  0(包括+0和-0)  NaN  空字符串('')
    Boolean(undefined) //false
    Boolean(null)        //false
    Boolean(false)       //false
    Boolean(0)           //false
    Boolean(NaN)         //false
    Boolean('')          //false
     Boolean([]) //true
    Boolean({})          //true
    Boolean(new Date())  //true
    

    六、理解值类型和引用类型

    a 声明变量时不同的内存分配:

    1)原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。

    这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值。

    2)引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。

    这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。
    地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

    b 不同的内存分配机制也带来了不同的访问机制

    1)在 JS 中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。

    2)而原始类型的值则是可以直接访问到的。

    c 复制变量时的不同

    1)原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的值而已,彼此都是独立的。

    2)引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。(复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)

    d 参数传递的不同(把实参复制给形参的过程)

    首先我们应该明确一点:ECMAScript 中所有函数的参数都是按值来传递的。
    但是为什么涉及到原始类型与引用类型的值时仍然有区别呢?还不就是因为内存分配时的差别。

    1)原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。

    2)引用值:对象变量它里面的值是这个对象在堆内存中的内存地址,这一点你要时刻铭记在心!

    因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。

    七、null 和 undefined 的区别

    定义

    Null 类型:Null 类型也只有一个特殊的值——null。从逻辑角度来看,null 值表示一个空对象指针。

    Undefined 类型:Undefined 类型只有一个值,即特殊的 undefined。在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined。

    null 和 undefined 的应用场景

    null 表示"没有对象",即该处不应该有值。典型用法是:

    (1) 作为函数的参数,表示该函数的参数不是对象。

    (2) 作为对象原型链的终点。

    console.log(null instanceof Object) // false
    

    undefined 表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:

    (1)变量被声明了,但没有赋值时,就等于 undefined。

    (2) 调用函数时,应该提供的参数没有提供,该参数等于 undefined。

    (3)对象没有赋值的属性,该属性的值为 undefined。

    (4)函数没有返回值时,默认返回 undefined。

    Number 转换的值

    Number(null)输出为 0, Number(undefined)输出为 NaN

    八、至少可以说出三种判断 JavaScript 数据类型的方式,以及他们的优缺点,如何准确的判断数组类型

    typeof

    • 适用场景

    typeof操作符可以准确判断一个变量是否为下面几个原始类型:

    typeof 'ConardLi' // string
    typeof 123 // number
    typeof true // boolean
    typeof Symbol() // symbol
    typeof undefined // undefined
    

    你还可以用它来判断函数类型:

    typeof function() {} // function
    
    • 不适用场景

      当你用typeof来判断引用类型时似乎显得有些乏力了:

    typeof [] // object
    typeof {} // object
    typeof new Date() // object
    typeof /^\d*$/ // object
    

    除函数外所有的引用类型都会被判定为object

    另外typeof null === 'object'也会让人感到头痛,这是在JavaScript初版就流传下来的bug,后面由于修改会造成大量的兼容问题就一直没有被修复...

    instanceof

    instanceof操作符可以帮助我们判断引用类型具体是什么类型的对象:

    ;[] instanceof Array // true
    new Date() instanceof Date // true
    new RegExp() instanceof RegExp // true
    

    我们先来回顾下原型链的几条规则:

    • 1.所有引用类型都具有对象特性,即可以自由扩展属性
    • 2.所有引用类型都具有一个__proto__(隐式原型)属性,是一个普通对象
    • 3.所有的函数都具有prototype(显式原型)属性,也是一个普通对象
    • 4.所有引用类型__proto__值指向它构造函数的prototype
    • 5.当试图得到一个对象的属性时,如果变量本身没有这个属性,则会去他的__proto__中去找

    [] instanceof Array实际上是判断Array.prototype是否在[]的原型链上。
    所以,使用instanceof来检测数据类型,不会很准确,这不是它设计的初衷:

    [] instanceof Object // true
    function(){}  instanceof Object // true
    

    另外,使用instanceof也不能检测基本数据类型,所以instanceof并不是一个很好的选择。

    toString

    上面我们在拆箱操作中提到了toString函数,我们可以调用它实现从引用类型的转换。

    每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

    const obj = {}
    obj.toString() // [object Object]
    

    注意,上面提到了如果此方法在自定义对象中未被覆盖toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。

    我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。

    jquery

    我们来看看jquery源码中如何进行类型判断:

    var class2type = {};
    jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
    function( i, name ) {
        class2type[ "[object " + name + "]" ] = name.toLowerCase();
    } );
    
    type: function( obj ) {
        if ( obj == null ) {
            return obj + "";
        }
        return typeof obj === "object" || typeof obj === "function" ?
            class2type[Object.prototype.toString.call(obj) ] || "object" :
            typeof obj;
    }
    
    isFunction: function( obj ) {
            return jQuery.type(obj) === "function";
    }
    

    原始类型直接使用typeof,引用类型使用Object.prototype.toString.call取得类型。
    判断数组类型可以用 Array.isArray(value) 或者 Object.prototype.toString.call(value)

    九、可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用

    因为JavaScript是弱类型的语言,所以类型转换发生非常频繁,上面我们说的装箱和拆箱其实就是一种类型转换。

    类型转换分为两种,隐式转换即程序自动进行的类型转换,强制转换即我们手动进行的类型转换。

    强制转换这里就不再多提及了,下面我们来看看让人头疼的可能发生隐式类型转换的几个场景,以及如何转换:

    类型转换规则

    如果发生了隐式转换,那么各种类型互转符合下面的规则:


    if 语句和逻辑语句

    if语句和逻辑语句中,如果只有单个变量,会先将变量转换为Boolean值,只有下面几种情况会转换成false,其余被转换成true

    null
    undefined
    ;('')
    NaN
    0
    false
    

    各种运数学算符

    我们在对各种非Number类型运用数学运算符(- * /)时,会先将非Number类型转换为Number类型;

    1 - true // 0
    1 - null //  1
    1 * undefined //  NaN
    2 * ['5'] //  10
    

    注意+是个例外,执行+操作符时:

    • 1.当一侧为String类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。
    • 2.当一侧为Number类型,另一侧为原始类型,则将原始类型转换为Number类型。
    • 3.当一侧为Number类型,另一侧为引用类型,将引用类型和Number类型转换成字符串后拼接。
    123 + '123' // 123123   (规则1)
    123 + null // 123    (规则2)
    123 + true // 124    (规则2)
    123 + {} // 123[object Object]    (规则3)
    

    ==

    使用==时,若两侧类型相同,则比较结果和===相同,否则会发生隐式转换,使用==时发生的转换可以分为几种不同的情况(只考虑两侧类型不同):

    • 1.NaN

      NaN和其他任何类型比较永远返回false(包括和他自己)。

    NaN == NaN // false
    
    • 2.Boolean

      Boolean和其他任何类型比较,Boolean首先被转换为Number类型。

    true == 1 // true
    true == '2' // false
    true == ['1'] // true
    true == ['2'] // false
    

    这里注意一个可能会弄混的点:undefined、nullBoolean比较,虽然undefined、nullfalse都很容易被想象成假值,但是他们比较结果是false,原因是false首先被转换成0

    undefined == false // false
    null == false // false
    
    • 3.String 和 Number

      StringNumber比较,先将String转换为Number类型。

    123 == '123' // true
    '' == 0 // true
    
    • 4.null 和 undefined

      null == undefined比较结果是true,除此之外,null、undefined和其他任何结果的比较值都为false

    null == undefined // true
    null == '' // false
    null == 0 // false
    null == false // false
    undefined == '' // false
    undefined == 0 // false
    undefined == false // false
    
    • 5.原始类型和引用类型

      当原始类型和引用类型做比较时,对象类型会依照ToPrimitive规则转换为原始类型:

    '[object Object]' == {} // true
    '1,2,3' == [1, 2, 3] // true
    

    来看看下面这个比较:

    ;[] == ![] // true
    

    !的优先级高于==![]首先会被转换为false,然后根据上面第三点,false转换成Number类型0,左侧[]转换为0,两侧比较相等。

    ;([null] == false[undefined]) == // true
      false // true
    

    根据数组的ToPrimitive规则,数组元素为nullundefined时,该元素被当做空字符串处理,所以[null]、[undefined]都会被转换为0
    所以,说了这么多,推荐使用===来判断两个值是否相等...

    一道有意思的面试题

    一道经典的面试题,如何让:a == 1 && a == 2 && a == 3
    根据上面的拆箱转换,以及==的隐式转换,我们可以轻松写出答案:

    const a = {
      value: [3, 2, 1],
      valueOf: function() {
        return this.value.pop()
      }
    }
    

    十、出现小数精度丢失的原因,JavaScript 可以存储的最大数字、最大安全数字,JavaScript 处理大数字的方法、避免精度丢失的方法

    出现小数精度丢失的原因

    计算机的二进制实现和位数限制有些数无法有限表示。就像一些无理数不能有限表示,如 圆周率 3.1415926...,1.3333... 等。JS 遵循 IEEE 754 规范,采用双精度存储(double precision),占用 64 bit。如图

    意义

    1 位用来表示符号位

    11 位用来表示指数

    52 位表示尾数

    浮点数,比如

    1

    2

    0.1 >> 0.0001 1001 1001 1001…(1001 无限循环)

    0.2 >> 0.0011 0011 0011 0011…(0011 无限循环)

    此时只能模仿十进制进行四舍五入了,但是二进制只有 0 和 1 两个,于是变为 0 舍 1 入。这即是计算机中部分浮点数运算时出现误差,丢失精度的根本原因。
    JS 的最大和最小安全值可以这样获得:

    console.log(Number.MAX_SAFE_INTEGER) //9007199254740991
    console.log(Number.MIN_SAFE_INTEGER) //-9007199254740991
    

    对于整数,前端出现问题的几率可能比较低,毕竟很少有业务需要需要用到超大整数,只要运算结果不超过 Math.pow(2, 53) 就不会丢失精度。如果实在是超过最大安全数字了,那就用 BigInt(Number)计算。

    对于小数,前端出现问题的几率还是很多的,尤其在一些电商网站涉及到金额等数据。解决方式:把小数放到位整数(乘倍数),再缩小回原来倍数(除倍数),也就是说,尽量在业务中避免处理小数

    相关文章

      网友评论

        本文标题:变量和类型

        本文链接:https://www.haomeiwen.com/subject/tqnepctx.html