【自检清单】JS基础-变量与类型

625 阅读15分钟

打算以一名【合格】前端工程师的自检清单为纲整理自己的知识体系,由于是整理,风格偏简洁,有疑惑、意见建议可前往小弟博客交流,后续的整理也会在博客及时更新,博客地址github.com/logan70/Blo…

JavaScript 数据类型

7种原始类型(Primitive data type)

原始类型的值本身都是不可变的(immutable)

布尔类型

true / false

Null类型

null,特指对象的值未设置,是一个字面量,不是全局属性。

Undefined类型

undefined,是全局属性,不是保留字,可使用void操作符代替。

数字类型

基于IEEE754标准的双精度64位二进制格式的值。

  • 展开查看数字类型特殊常量
    • 检查值是否大于或小于+-Infinity,可使用常量Number.MAX_VALUENumber.MIN_VALUE
    • 双精度浮点数的取值范围是Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER

BigInt类型

可以用任意精度表示整数。目的是为了安全地存储和操作大整数,甚至可以超过数字的安全整数限制。

  • 展开查看BigInt类型创建方式

    通过整数后加n或调用BigInt函数创建。

    const bigNum1 = 123n          // 123n
    const bigNum2 = BigInt(456)   // 456n
    

字符串类型

  • 由一组16位的无符号整数值(即UTF-16)构成。

  • 每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode 码点大于0xFFFF的字符),JavaScript 会认为它们是两个字符。

符号类型

Symbol,唯一的并且是不可修改的, 并且也可以用来作为Object的key的值。

Object类型

对象是指内存中的可以被标识符引用的一块区域。

  • 展开查看对象数据属性的特性(Attributes of a data property)
    特性 数据类型 描述 默认值
    [[Value]] 任何JS类型 包含这个属性的数据值。 undefined
    [[Writable]] Boolean 如果该值为 false,则该属性的 [[Value]] 特性 不能被改变。 true
    [[Enumerable]] Boolean 如果该值为 true,则该属性可以用 for...in 循环来枚举。 true
    [[Configurable]] Boolean 如果该值为 false,则该属性不能被删除,并且不能被转变成一个数据属性。 true
  • 展开查看对象访问器属性的特性
    特性 数据类型 描述 默认值
    [[Get]] 函数对象或者 undefined 该函数使用一个空的参数列表,能够在有权访问的情况下读取属性值。 undefined
    [[Set]] 函数对象或者 undefined 该函数有一个参数,用来写入属性值。 undefined
    [[Enumerable]] Boolean 如果该值为 true,则该属性可以用 for...in 循环来枚举。 true
    [[Configurable]] Boolean 如果该值为 false,则该属性不能被删除,并且 除了 [[Value]] 和 [[Writable]] 以外的特性都不能被改变。 true

“标准”对象

即键和值之间的映射,键是字符串或者Symbol,值是任意JS类型。

其他对象

  • Function对象: 函数,附带可被调用功能的常规对象。

  • Promise对象: 代表了未来将要发生的事件,用来传递异步操作的消息。

  • Proxy对象: 用于定义基本操作的自定义行为。

  • Reflect对象: 提供拦截 JavaScript 操作的方法。

  • Date对象: 日期对象构造函数,也有静态属性和方法。

  • Array对象: 数组,使用整数作为键(integer-key-ed)属性和长度(length)属性之间关联的常规对象。

  • TypedArray对象: 类型数组,提供了基本二进制数据缓冲区的类数组视图的对象。包括Int8ArrayUint8ArrayUint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFload64ArrayBigInt64ArrayBigUint64Array

  • 键控集: 包括MapWeakMapSetWeakSet

  • JSON(JavaScript Object Notation)对象: 用于结构化数据。

  • Math对象: 数学相关属性方法的集合

  • RegExp对象: 正则表达式构造函数

  • 各错误对象: 包括ErrorEvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError

对象的底层数据结构

JavaScript中的对象是基于哈希表结构的。

哈希表(Hash table)、哈希函数(Hash Function)与哈希碰撞(Hash Collision)

哈希表 又称散列表,根据键直接访问在内存存储位置的数据结构。通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。

哈希函数 又称散列函数散列算法,上述映射函数即为哈希函数,是一个表示键和内存存储值位置的映射关系的函数。常见的构造哈希函数的方法有直接定址法除留余数法随机数法等。

哈希碰撞 又称哈希冲突,指不同键经过哈希函数计算后得到相同哈希地址的情况。具有相同函数值的关键字对该哈希函数来说称做同义词

处理哈希碰撞

开放定址法

当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1...,直到找出不冲突的哈希地址pi,将相应元素存入其中。

优点:储空间更加紧凑,利用率高。
缺点:冲突元素间产生关联,无法直接删除,会破坏寻址链,只能在删除节点上作删除标记。

开放定址法处理哈希碰撞

拉链法

又称链地址法,将散列到同一个存储位置的所有元素保存在一个链表中。

优点:处理冲突简单,非同义词决不会发生冲突,因此平均查找长度较短。
缺点:指针需要额外的空间,降低构建哈希表时的效率。

拉链法处理哈希碰撞

查找效率

  • 二分查找: 复杂度为O(log2n),但只能用于有序列表。
  • 遍历查找: 复杂度为O(n)
  • 哈希表: 理想的哈希函数实现的哈希表,对其任意元素的查找速度始终为常数级,即O(1)

如果遭到恶意哈希碰撞攻击,拉链法会导致哈希表退化为链表,即所有元素都被存储在同一个节点的链表中,此时哈希表的查找速度=链表遍历查找速度=O(n)。

Symbol的应用及实现

Symbol

Symbol([description])函数会返回symbol类型的值。

  • description:可选的字符串。symbol的描述,可用于调试但不能访问symbol本身。
  • 每个从Symbol()返回的symbol值都是唯一的;
  • symbol是一种基本数据类型;
  • symbol类型唯一目的:作为对象属性的标识符。
const symbol1 = Symbol()
const symbol2 = Symbol('foo')

全局共享的Symbol

Symbol.for(key):全局symbol注册表中有与key对应的symbol则返回,否则在全局symbol注册表新建与key关联的symbol并返回。

Symbol.keyFor(symbol):获取全局symbol注册表中与某个 symbol 关联的键,没有则返回undefined

const globalSym = Symbol.for('foo')
expect(Symbol.keyFor(globalSym)).toBe('foo')

const localeSym = Symbol('bar')
expect(Symbol.keyFor(localeSym)).toBeUndefined()

Symbol特性

symbol的创建

  • 不能通过new关键字调用Symbol函数,因为禁止创建显式的 Symbol 包装器对象
expect(() => new Symbol('foo')).toThrowError(new TypeError('Symbol is not a constructor'))

expect(() => Symbol('foo')).not.toThrow()

symbol类型的识别

  • 使用typeof运算符来识别symbol类型
  • symbol是原始类型,无法使用instanceof进行识别
  • 如果想得到一个Symbol包装器对象,可以使用Object()函数。
const sym = Symbol('foo')

expect(typeof sym).toBe('symbol')
expect(sym instanceof Symbol).toBe(false)

const symObj = Object(sym)
expect(symObj instanceof Symbol).toBe(true)

symbol的类型转换

symbol类型值可显式转string类型或者boolean类型, 不能转number类型。

const sym = Symbol('foo')
expect(String(sym)).toBe('Symbol(foo)')
expect(Boolean(sym)).toBe(true)
expect(() => Number(sym))
  .toThrowError(new TypeError('Cannot convert a Symbol value to a number'))

对象symbol属性的获取

  • 对象的symbol属性在for...in迭代中不可枚举,也无法通过Object.keys/Object.getOwnPropertyNames获得。
  • 可以使用Object.getOwnPropertySymbols()对象自身的所有 Symbol 属性的数组。
  • Reflect.ownKeys()可以获取对象自身的所有可枚举、不可枚举及Symbol属性的数组。
const obj = {
  [Symbol('foo')]: 'foo',
  bar: 'bar'
}
const isSymbol = s => typeof s === 'symbol'
const hasSymbol = arr => arr.some(isSymbol)

let canGetSymbolByForIn = false
for (k in obj) {
  if (isSymbol(k)) {
    canGetSymbolByForIn = true
    break
  }
}
expect(canGetSymbolByForIn).toBe(false)

expect(hasSymbol(Object.keys(obj))).toBe(false)
expect(hasSymbol(Object.getOwnPropertyNames(obj))).toBe(false)
expect(Object.getOwnPropertySymbols(obj).map(String)).toEqual(['Symbol(foo)'])
expect(Reflect.ownKeys(obj).map(String)).toEqual(['bar', 'Symbol(foo)'])

Symbol的应用

使用Symbol作为对象属性名

对象次要的元信息属性或者不想被迭代的属性,可以使用Symbol来作为属性名,相较Object.defineProperty去指定enumerable: false比较简洁。

const META_PROP = Symbol('meta')
const obj = {
  [META_PROP]: '次要信息',
  name: 'logan',
  age: 18,
}

expect(Object.keys(obj)).toEqual(['name', 'age'])

使用Symbol代替常量

好处是不用考虑常量值重复,常量较多时比较有用。

// before
const TYPE_AUDIO = 'AUDIO'
const TYPE_VIDEO = 'VIDEO'

// after
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()

使用Symbol模拟私有属性/方法

注意: 仅用作模拟,不要尝试使用 Symbol 存储对象中需要真正私有化的值,如密码等属性,对象上所有的 Symbols 都可以直接通过 Object.getOwnPropertySymbols() 获得!

// lady.js
const AGE = Symbol('age')
const GET_AGE = Symbol('getAge')
export class Lady {
  constructor(username, age) {
    this.username = username
    this[AGE] = age
  }

  [GET_AGE]() {
    return this[AGE]
  }
}

// foo.js
import { Lady } from './lady'
const lady = new Lady('lucy', 18)

expect(lady[Symbol('age')]).toBeUndefined()
expect(() => lady[Symbol('getAge')]()).toThrowError('is not a function')

const ladyAgeKey = Object.getOwnPropertySymbols(lady)[0]
const ladyAge = lady[ladyAgeKey]
expect(ladyAge).toBe(18)

内置Symbols

内置的Symbols被用作数组、字符串等原生对象以及 JavaScript 引擎内部的方法名,这样就避免了被意外重写的可能。

介绍几个常用的内置Symbol,其余的可前往MDN-Symbol了解

Symbol.iterator

用于定义对象的迭代器,,可被for...of循环及数组展开操作符使用。

const myIterable = {
  *[Symbol.iterator]() {
    yield 1
    yield 2
    yield 3

  }
}
expect([...myIterable]).toEqual([1, 2, 3])

Symbol.hasInstance

构造函数用来识别一个对象是否为它的实例。被 instanceof 使用。

class MyArray {  
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance)
  }
}

expect([] instanceof MyArray).toBe(true)

Symbol.toPrimitive

用于定义将对象转换为原始值时的行为。

  • 执行 +obj ,会调用 obj[Symbol.toPrimitive]('number')
  • 执行 `${obj}` ,会调用 obj[Symbol.toPrimive]('string')
  • 执行 字符串连接,如'' + obj,会调用 obj[Symbol.toPrimitive]('default')
const obj = {
  [Symbol.toPrimitive](hint) {
    console.log(hint)
    return hint === 'number'
      ? 10
      : `hint is ${hint}`
  }
}

expect(+obj).toBe(10)
expect(`${obj}`).toBe('hint is string')
expect(obj + '').toBe('hint is default')

Symbol.toStringTag

用于对象的默认描述的字符串值。被Object.prototype.toString()使用。

class Person {
  get [Symbol.toStringTag]() {
    return 'Person'
  }
}

expect(Object.prototype.toString.call(new Person)).toBe('[object Person]')

实现Symbol

typeof Symbol() === 'symbol'、对象symbol属性不可迭代等特性无法模拟。

我们围绕最重要的特性,也是symbol类型的唯一目的--作为对象属性的标识符来进行模拟。

// 自定义symbol对象的原型
const symbolProto = {}

// 设置对象属性时会调用toString,返回__name__属性
Object.defineProperties(symbolProto, {
  toString: generatePrivateDescriptor(function() { return this.__name__ }),
})

export default function SymbolPolyfill(description) {
  // 实现禁止使用new操作符生成Symbol
  if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor')
  // symbol描述为undefined时为空,其他情况均强制转换为字符串
  description = description === undefined ? '' : String(description)
  symbol = Object.create(symbolProto)
  return Object.defineProperties(symbol, {
    __name__: generatePrivateDescriptor(generateName(description)),
  })
}

// 生成唯一的字符串
const nameRecorder = {}
function generateName(desc) {
  let postfix = 0
  while (nameRecorder[desc + postfix]) postfix++
  nameRecorder[desc + postfix] = true
  return '@@' + desc + postfix
}

// 生成Object.defineProperty的描述对象
function generatePrivateDescriptor(value) {
  return {
    value,
    configruable: false,
    enumerable: false,
    writable: false
  }
}
// 测试
import SymbolPolyfill from './SymbolPolyfill'

const sym1 = SymbolPolyfill('foo')
const sym2 = SymbolPolyfill('foo')

const obj = {}
obj[sym1] = 1
obj[sym2] = 2

expect(sym1 in obj).toBe(true)
expect(sym2 in obj).toBe(true)
expect(obj[sym1]).not.toBe(true)

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

基本类型变量的存储

  • 基本类型变量存储在栈内存中;
  • JS中基本类型值是不可变的,故基本类型变量改变时都会为变量重新分配内存并存储值。

下面例子说明了基本类型变量的声明、赋值及改变的过程。

// Step 1. `myNumber` -> Address: 0012CCGWH80 -> Value: 23
let myNumber = 23

// Step 2. `newVar` -> Address: 0012CCGWH80 -> Value: 23
let newVar = myNumber

// Step 3. `myNumber` -> Address: 0034AAAAH23 -> Value: 24
myNumber = myNumber + 1

基本类型变量的存储

基本类型变量的存储

基本类型变量的存储

栈内存 与 堆内存

JavaScript内存模型中,内存空间分为 栈内存(Stack)堆内存(Heap) 两种。

展开查看栈内存、堆内存的特点

栈内存特点

  • 存储的值大小固定
  • 空间较小
  • 可以直接操作其保存的变量,运行效率高
  • 由系统自动分配存储空间

堆内存特点

  • 存储的值大小不定,可动态调整
  • 空间较大,运行效率低
  • 无法直接操作其内部存储,使用引用地址读取
  • 通过代码进行分配空间

引用类型变量的存储

  • 引用类型变量(即Object类型)存储在堆内存中;
  • 堆内存中存储的引用类型变量值是可变的。

下面例子说明了引用类型变量的声明、赋值及改变的过程。

// Step 1. `myArray` -> HeapAddress: 22VVCX011 -> Value: []
let myArray = []

// Step 2. `myArray` -> HeapAddress: 22VVCX011 -> Value: ['first', 'second', 'third']
myArray.push('first')
myArray.push('second')
myArray.push('third')

引用类型变量的存储
引用类型变量的存储

变量的比较与传递

变量的比较,是按变量在栈内存中的值进行比较,

变量的拷贝与变量作为函数参数进行传递,也是按变量在栈内存中的值进行传递。

  • 对于基本类型变量来说,栈内存中的值即为其值本身;
  • 对于引用类型变量来说,栈内存中的值即为指向堆内存中引用类型值的地址。
// 变量比较
const num1 = 12
const num2 = 12

expect(num1 === num2).toBe(true)

const obj1 = { foo: 'foo' }
const obj2 = obj1
const obj3 = { foo: 'foo' }

expect(obj1 === obj2).toBe(true)
expect(obj1 === obj3).toBe(false)
// 变量传递
const changeNum = (num) => num++
const changeObj = (obj) => (obj = { foo: 'bar' })
const changeObjProp = (obj) => (obj.foo = 'bar')

const num = 1

changeNum(num)
expect(num).toBe(1)

const obj1 = { foo: 'foo' }
const obj2 = { foo: 'foo' }

changeObjProp(obj1)
expect(obj1).toEqual({ foo: 'bar' })

changeObj(obj2)
expect(obj2).toEqual({ foo: 'foo' })

基本类型的装箱与拆箱

包装类型

为了便于操作基本类型值,JavaScript定义了 BooleanNumberStringSymbolBigInt 几种包装类型(属于引用类型),每种包装类型都有一种对应的基本类型。

操作基本类型时,JS引擎会自动创建基本类型对应的包装类型。

展开查看模拟代码
const name = 'Logan Lee'
const firstName = name.substr(6)

// 执行时相当于
const name = 'Logan Lee'
const nameObj = new String(name)
const firstName = nameObj.substr(6)
nameObj = null

当然我们也可以自己通过 new 操作符来创建包装类型。

SymbolBigInt 不能作构造函数用,可以配合Object构造函数来创建对应的包装类型。

expect(new Boolean(true)).toBeInstanceOf(Boolean)
expect(new Number(1)).toBeInstanceOf(Number)
expect(new String('Logan')).toBeInstanceOf(String)
expect(Object(Symbol('foo'))).toBeInstanceOf(Symbol)
expect(Object(BigInt(30))).toBeInstanceOf(BigInt)

装箱操作

定义

通过val.propval[expression]的格式进行属性访问时,如果val为基本类型变量,则会将其转换为对应的内置对象类型。

上述过程叫做基本数据类型的装箱操作,各类型变量装箱结果见下表。装箱标准定义见 ECMAScript#ToObject

变量类型 装箱结果
Undefined 抛出 TypeError 异常
Null 抛出 TypeError 异常
Boolean 返回对应的 Boolean对象
Number 返回对应的 Number对象
String 返回对应的 String对象
Symbol 返回对应的 Symbol对象
BigInt 返回对应的 BigInt对象
Object 返回对象本身

示例

expect(() => (undefined).x).toThrowError()
expect(() => (null).x).toThrowError()

expect(() => (true).toString()).toBe('true')
expect(() => (1).toFixed(1)).toBe('1.0')
expect(() => ('abc').substr(1)).toBe('bc')
expect(() => (Symbol('foo')).description).toBe('foo')

拆箱操作

在对引用类型(包括)变量进行 数学运算字符串拼接模板字符串内计算 等操作时,JS引擎会尝试将其转换为基本类型,这个过程叫做拆箱。

ES6之前,从引用类型到基本类型的转换会调用引用类型的toStringvalueOf两个方法,调用顺序根据场景不同而不同,不作赘述。

ES6之后,统一使用 [Symbol.toPrimitive] 来定义将对象转换为原始值时的行为,函数接收一个字符串参数 hint ,表示要转换到的原始值的预期类型。

const obj1 = {}
const obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return 10
    if (hint === 'string') return 'Logan'
    return 'default'
  }
}

expect(+obj1).toBeNaN()
expect(+obj2).toBe(10)

expect(`${obj1}`).toBe('[object Object]')
expect(`${obj2}`).toBe('Logan')

expect('' + obj1).toBe('[object Object]')
expect('' + obj2).toBe('default')

Null 与 Undefined

  • null表示已被赋值,但值为空,即将一个变量显式赋值为null是正常的
  • undefined表示已声明还未定义的变量 或 对象上不存在的属性,故将变量或属性显式赋值为undefined是不正常的
  • nullundefined转换为布尔值均为false
  • null转换为数值为0undefined转换为数值为NaN
  • null == undefined,因为ECMAScript定义如此,并没有发生隐式类型转换

    ECMAScript中定义 "If x is null and y is undefined, return true."
    详见ECMAScript#abstract-equality-comparison

  • null !=== undefined,二者不是同一数据类型
// bad
let a = undefined

// good
let a

判断JavaScript数据类型

实现判断数据类型功能

function getType(v) {
  // `null`
  if (v === null) return 'null'
  
  const baseType = typeof v
  // `undefined`、`number`、`string`、`boolean`、`symbol`、`bigInt`、`function`
  if (baseType !== 'object') return baseType

  // `arguments`、`error`、`date`、`regExp`、`object`
  // `map`、`set`、`weakmap`、`weakset`
  // 基本类型的包装类型按照其基本类型返回
  const builtinType = Object.prototype.toString.call(v)
    .slice(8, -1).toLocaleLowerCase()

  return builtinType
}

typeof

可判断类型UndefinedNumberStringBooleanSymbolBigIntFunction

坑点typeof null === 'object',JS历史Bug,修改后造成大量兼容问题,故遗留至今。

expect(typeof undefined).toBe('undefined')
expect(typeof 1).toBe('number')
expect(typeof 'Logan').toBe('string')
expect(typeof true).toBe('boolean')
expect(typeof Symbol()).toBe('symbol')
expect(typeof BigInt(123)).toBe('bigint')
expect(typeof (() => {})).toBe('function')

instanceof

instanceof 操作符左侧为引用类型,右侧为构造函数,作用是检测右侧构造函数的原型是否在左侧对象的原型链上出现过,故不太适合用作数据类型判断。

更多有关原型与原型链的内容,可前往 深入JavaScript系列(六):原型与原型链 了解。

expect([] instanceof Array).toBe(true)
expect([] instanceof Object).toBe(true)

expect(null instanceof Object).toBe(false)

Object.prototype.toString

由于基本类型的包装对象以及除Object外的其他引用类型大都重写了toString方法,导致我们无法得到预期的效果,故使用Object.prototype.toString,通过call调用将this指向我们想判断类型的变量或值。ECMAScript相关描述详见 ECMAScript#Object.prototype.toString

可以通过定义对象的[Symbol.toStringTag]属性来自定义Object.prototype.toString.call()时的表现,详见Symbol的应用及实现

将各个类型传入Object.prototype.toString.call()的表现如下表:

*Error代表所有错误类对象

传入变量类型 返回结果
Undefined '[object Undefined]'
Null '[object Null]'
Boolean '[object Boolean]'
Number '[object Number]'
String '[object String]'
Symbol '[object Symbol]'
BigInt '[object BigInt]'
Object '[object Object]'
Array '[object Array]'
Function '[object Function]'
Arguments '[object Arguments]'
Set '[object Set]'
Map '[object Map]'
WeakSet '[object WeakSet]'
WeakMap '[object WeakMap]'
Date '[object Date]'
RegExp '[object RegExp]'
*Error '[object Error]'

隐式类型转换

    1. 在加法运算时
    • 1-1 若两操作数中有字符串,则优先转字符串;
    • 1-2 若两操作数都不是字符串,则优先转数字,若是引用类型转换得到字符串,则回到1-1。
    1. 其他运算及==比较符,均优先转数字,若是转换得到字符串,再尝试转数字,转成则计算,转不成则NaN
    1. null转数字为0,undefined转数字为NaN
    1. 引用类型转数字valueOf优先级大于toString,转字符串toString优先级大于valueOf,先调用优先级高的,得不到基本类型再调用优先级低的。
    1. +varible强制转数字,失败则为NaN
    1. 多个加号从左到右依次计算。
// 满足1-1,左边的1转字符串'1',相加为'11'
1 + '1' // '11'

// 首先![]优先计算为false,本题实质是 [] == false
// 满足规则1-2,[]尝试转数字,用valueOf,还是[],不行
// 再用toString,得到空字符串,此时为 '' == false
// ''转数字为0,false转数字为0,所以此题为true
[] == ![] // true

// true + true 符合1-2,即1 + 1 = 2
// 2 + false 符合1-2, 即 2 + 0 = 2
// 2 + '100' 符合1-1,即 '2' + '100' = '2100'
true + true + false + '100' // '2100'

// 符合1-2,数组转数字,调用valueOf失败
// 调用toString得到 '1,2,3,4'
// 4 + '1,2,3,4' 符合1-1,最终为 '41,2,3,4' 
4 + [1, 2, 3, 4]

// 符合1-2,对象转数字,调用valueOf失败
// 调用toString得到 '[object Object]'
// 1 + '[object Object]',符合1-1,最终结果为'1[object Object]'
1 + {}

// +'b'视为整体,强制转数字,失败,返回NaN
// 'a' + NaN,符合1-1,返回'aNaN'
'a' + + 'b'

数字精度丢失(0.1 + 0.2 = 0.30000000000000004)

IEEE754

JavaScript中的Number类型是基于 IEEE 754 标准的双精度 64 位二进制格式的值。

  • 符号位:1位,标识数值正负,0为正,1为负
  • 指数部分:11位,表示范围为0~2047,减去偏移常数bias为1023,即实际范围为-1023~1024
    • 展开指数部分详细讲解

      指数偏移常量:计算指数时要减去的常量。 指数位数若为e,指数偏移常量bias则为 2e-1,双精度64位浮点数中,指数位数为11位,故偏移常量为1023,指数最终取值为 0 - 1023 ~ 2047 - 1023,即-1023~1024

      特殊指数:指数全0或全1有特殊含义,不算正常指数。

      • 指数全0,尾数全0,表示0。根据符号位不同可以分为+0-0
      • 指数全0,尾数不为全0,这些数是非规范数,即尾数部分假设前面为0,而不是1。此时指数取最后一位为1时的值,64位双精度浮点数格式中为-1022
      • 指数全1,尾数全0,表示无穷大,即Infinity。根据符号位不同可以分为+Infinity-Infinity
      • 指数全1,尾数不为全0,表示NaN,即Not a Number,不是数。
  • 尾数部分:52位,二进制只有0和1,一个数值最终都可用 1.xxx * 2 e 表示,故尾数部分表示小数点后的部分,小数点前默认有1。

0.1 + 0.2 = 0.30000000000000004

JS使用双精度 64 位二进制格式存储数值,所以先要将0.1和0.2转换为二进制。

0.1和0.2转换为二进制后是无限循环的,但是存储位是有限的,所以超出的部分要作“零舍一入”,这是第一步精度丢失

相加时,由于两数指数级不相等,要进行“对位”操作,而且相加后产生了进位,这两个原因导致存储位不够用,超出部分又要“零舍一入”,这是第二次精度丢失,导致计算结果出现偏差。

具体计算分析过程参考 IEEE754 浮点数格式 与 Javascript number 的特性

Number上各个静态常量的理解

指数不取全0或全1的原因详见文章上方指数部分详细讲解

Number.MAX_VALUENumber.MIN_VALUE

Number.MAX_VALUE:可表示的最大正值: 当符号位为0、指数位除最后一位全为1、尾数位全为1时,为可表示的最大正值。 Number.MIN_VALUE:可表示的最小正值: 当符号位为0、指数位全为0、尾数最后一位为1时,为可表示的最小正值。

// Number.MAX_VALUE
expect((2 - Math.pow(2, -52)) * Math.pow(2, 1023)).toBe(Number.MAX_VALUE)
// Number.MIN_VALUE
expect(Math.pow(2, -52) * Math.pow(2, -1022)).toBe(Number.MIN_VALUE)

Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER

Number.MAX_SAFE_INTEGER:可表示的最大准确整数: 当符号位为0、指数实际值为52、尾数位全为1,即尾数每一位都表示整数时,为可表示的最大准确整数。 Number.MIN_SAFE_INTEGER:可表示的最小准确整数: 当符号位为1、指数实际值为52、尾数位全为1,即尾数每一位都表示整数时,为可表示的最小准确整数。

// Number.MAX_SAFE_INTEGER
expect((2 - Math.pow(2, -52)) * Math.pow(2, 52)).toBe(Number.MAX_SAFE_INTEGER)
// Number.MIN_SAFE_INTEGER
expect((-1) * (2 - Math.pow(2, -52)) * Math.pow(2, 52)).toBe(Number.MIN_SAFE_INTEGER)

Number.EPSILON

大于1的最小可表示数与1的差:符号位为0,指数实际值为0,尾数最后一位为1时的数减去1,为Number.EPSILON。即 2-52

// Number.EPSILON
expect((1 + Math.pow(2, -52)) - 1).toBe(Number.EPSILON)