打算以一名【合格】前端工程师的自检清单为纲整理自己的知识体系,由于是整理,风格偏简洁,有疑惑、意见建议可前往小弟博客交流,后续的整理也会在博客及时更新,博客地址github.com/logan70/Blo…。
JavaScript 数据类型
7种原始类型(Primitive data type)
原始类型的值本身都是不可变的(immutable)
布尔类型
true
/ false
。
Null类型
null
,特指对象的值未设置,是一个字面量,不是全局属性。
Undefined类型
undefined
,是全局属性,不是保留字,可使用void
操作符代替。
数字类型
基于IEEE754标准的双精度64位二进制格式的值。
-
展开查看数字类型特殊常量
- 检查值是否大于或小于
+-Infinity
,可使用常量Number.MAX_VALUE
和Number.MIN_VALUE
。 - 双精度浮点数的取值范围是
Number.MIN_SAFE_INTEGER
和Number.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对象: 类型数组,提供了基本二进制数据缓冲区的类数组视图的对象。包括
Int8Array
、Uint8Array
、Uint8ClampedArray
、Int16Array
、Uint16Array
、Int32Array
、Uint32Array
、Float32Array
、Fload64Array
、BigInt64Array
、BigUint64Array
。 -
键控集: 包括
Map
、WeakMap
、Set
、WeakSet
。 -
JSON(JavaScript Object Notation)对象: 用于结构化数据。
-
Math对象: 数学相关属性方法的集合
-
RegExp对象: 正则表达式构造函数
-
各错误对象: 包括
Error
、EvalError
、RangeError
、ReferenceError
、SyntaxError
、TypeError
、URIError
。
对象的底层数据结构
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定义了 Boolean
、Number
、String
、Symbol
、BigInt
几种包装类型(属于引用类型),每种包装类型都有一种对应的基本类型。
操作基本类型时,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
操作符来创建包装类型。
Symbol
、BigInt
不能作构造函数用,可以配合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.prop
或val[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
之前,从引用类型到基本类型的转换会调用引用类型的toString
和valueOf
两个方法,调用顺序根据场景不同而不同,不作赘述。
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
是不正常的null
和undefined
转换为布尔值均为false
null
转换为数值为0
,undefined
转换为数值为NaN
null == undefined
,因为ECMAScript
定义如此,并没有发生隐式类型转换ECMAScript中定义 "If x is null and y is undefined, return true."
详见ECMAScript#abstract-equality-comparisonnull !=== 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
可判断类型:Undefined
、Number
、String
、Boolean
、Symbol
、BigInt
、Function
。
坑点: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-2 若两操作数都不是字符串,则优先转数字,若是引用类型转换得到字符串,则回到1-1。
-
- 其他运算及
==
比较符,均优先转数字,若是转换得到字符串,再尝试转数字,转成则计算,转不成则NaN
;
- 其他运算及
-
null
转数字为0,undefined
转数字为NaN
;
-
- 引用类型转数字
valueOf
优先级大于toString
,转字符串toString
优先级大于valueOf
,先调用优先级高的,得不到基本类型再调用优先级低的。
- 引用类型转数字
-
+varible
强制转数字,失败则为NaN
;
-
- 多个加号从左到右依次计算。
// 满足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
,不是数。
- 指数全0,尾数全0,表示
-
- 尾数部分: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_VALUE
和 Number.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_INTEGER
和 Number.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)