类型、值和变量(一)
类型
众所周知,JavaScript的类型可以分为两类:
- 原始类型:字符(String),数字(Number),布尔(Boolean),null,undefined,符号(Symbol),BigInt。
- 对象类型:即任何不是原始类型的都是对象,普通对象,数组,Map,Set,function等等
在JavaScript中数组是特殊的对象,普通对象是命名值的无序集合(但是遍历有序,对 对象命名值进行遍历有一套规则的算法,在后面章节再深入讨论),而数值是属性值为数字的有序集合(当然你完全可以在其身上挂载其他属性,后面针对数组的章节再详细解释),另外JavaScript还提供了很多其他有用的对象,包括,正则(RegExp)日期(Date),Error对象等等。需要特别注意的是:
JavaScript的对象类型是可以被修改的,而原始类型是不能被修改的
JavaScript
// 原始类型不可修改,相当于另外赋值
let a = 1
let b = a
b = 2
console.log(a) // 1
JavaScript
// 对象类型可被修改
let a = {p:1}
let b = a
b.p = 2
console.log(a.p) // 2
相信这对于所有JavaScript工程师来说都非常熟悉,为什么会这样?因为 在JavaScript中原始类型是按 值访问 ,对象类型是按 引用访问 的 ,好像越说越迷?要真正了解就要涉及到内存的堆内存和栈内存的知识了, 先看图
在计算机编程中一般将内存分为两种,一种是 堆(Heap)内存 ,一种是 栈(Stack)内存
堆(Heap):
是动态分配内存的一种数据结构,堆内存的分配由开发人员手动控制,通过new关键字创建对象时会在堆上分配内存,而堆内存的释放由 垃圾回收机制 自动处理,当一个对象无引用时会被垃圾回收机制自动清掉。
垃圾回收机制在实现上有两种方式:
- 引用计数:最常用(目前主流浏览器的回收机制),当变量进入上下文时,会被加上存在于上下文中的标记,离开上下文时会加上离开上下文的标记。
- 标记清除:不常用,对每个值记录被引用的次数,为0时清除,但是若遇到循环引用时会导致失效。
由于垃圾回收机制的存在,对代码的优化策略就变得很重要了,在后面单独出一章聊一聊垃圾回收机制及其优化策略
堆内存是无序的,对其进行访问时,只能用其指针按 引用访问,它的大小取决于浏览器或运行环境的内存限制。 对于上面的代码来说,实质上我们复制的是变量a指向的堆内存中存放的对象的指针,所以,a和b都指向了该对象,这种访问形式就叫按引用访问,所以该赋值本质上是 "赋址" 。而指向该堆内存的指针存放于栈内存中。
栈(Stack)
栈是一种先进后出的数据结构,他的空间是连续的,栈内存主要用于存储函数的调用栈、基本数据类型和局部变量等,由于原始类型值的大小基本是固定的,所以将其放到栈内存中
以下是一些常见的基本数据类型及其在栈内存中的大致占用空间情况:
- 数字(Number) :通常占用 8 字节的内存空间。这是因为 JavaScript 使用 IEEE 754 标准表示数字,包括整数和浮点数。
- 布尔值(Boolean) :通常占用 4 字节的内存空间。尽管布尔值只有两个可能的取值(true 或 false),但为了对齐内存和处理方便,会占用一定的空间。
- 空值(null) 和 未定义(undefined) :通常占用 4 字节的内存空间。这两者表示变量没有值,所以它们在内存中的表示相对较小。
- 字符串(String) :字符串的占用空间取决于字符串的长度。每个字符通常占用 2 个字节的内存空间,但某些字符(如 Unicode 字符)可能会占用更多空间。
另外栈内存的分配和释放由 编译器和解释器 自动处理。
说到这想到另外一个话题就是JavaScript到底是编译型语言还是解释型语言,《你不知道的JavaScript》作者认为其是编译型语言,而大多数人认为其是一种解释型语言,我个人赞同于前者,虽然 JavaScript 在浏览器环境中通常被解释执行,但实际上现代的 JavaScript 引擎会对代码进行编译和优化,以提高执行效率。这种即时编译和优化的过程帮助 JavaScript 在现代 web 应用中实现了更好的性能。
从技术角度上来讲,只有JavaScript对象才有方法,但是除了null和undefined以外,其他原始类型就像对象一样也提供了一套方法。
数值
JavaScript中的数值采用IEEE754标准(Java,C#以及大多数现代编程语言采用这种格式表示double类型的数值,会造成小数精读缺失),该标准能表示的最大整数是+-1.7976931348623157x10^308,最小整数是+-5x10^(-324),在这种标准下我们可以 精确 的表示2^53 到2^-53之间的所有整数,如果超出这个范围会在末尾有精读损失,在日常开发中,这个范围已经足够应对几乎所有场景了,如果想更精确的表示更大的数字,则可以使用ES2020提出的bigInt类型(后面讨论)。
对于整数来说,除了十进制,你也可以采用二进制(0b)、八进制(0o)或者十六进制(0x)来表示一个整数。
注意 二进制和八进制的表示方法只能在ES6之后使用
对于浮点数,你可以使用指数计数法来表示。
对于一些比较长的数字,直观读起来不太方便,你可以使用 数字分隔符 来表示,像下面这样
javascript
let a = 1_000_000_000 //怎么分都行,只要你读起来方便 1_0000_000_00 也可以
let b = 0x12_AB_CD_EF
let c = 0.12_3_4_5
很有意思吧。
Math对象
JavaScript提供了 Math 对象,来实现一些复杂的运算,我们来看看他有哪些方法:
ES6以前:
方法 | 描述 |
---|---|
Math.pow(x,y) | 求x的y次方 |
Math.ceil(x) | 向上舍入到整数 |
Math.floor(x) | 向下舍入到整数 (作者经常把这两个搞混,一个记忆方法是ceil最后像1,所以向上舍,floor中的oo像0所以向下取),以上两个方法对于负数来说是相反的,可用Math.trunc()方法去除小数 |
Math.round(x) | 四舍五入 |
Math.abs(x) | 取绝对值 |
Math.max(x,y,z) | 取最大值 |
Math.min(x,y,z) | 取最小值 |
Math.random() | 生成[0,1) 之间的伪随机数,Math.random*(max-min)+min 生成[min,max)范围的伪随机数 |
Math.PI | π,圆周率 |
Math.E | e,自然对数 |
Math.sqrt(x) | x的平方根 |
Math.pow(x,1/y) | x的y次方根 |
Math.sin(x) Math.cos(x) Math.atan(x) | x的三角函数 |
Math.log(x) | x的自然对数 |
Math.log(x)/Math.LN10 | 以10为底的x的对数 类似的还有Math.LN2 |
Math.exp(x) | Math.E的x次方 |
ES6新增
方法 | 描述 |
---|---|
Math.cbrt(x) | 求x的立方根 |
Math.hypot(x,y...) | 求所有参数的平方和的平方根 |
Math.log10(x) | 以10为底数 |
Math.log2(x) | 以2为底数 |
Math.log1p(x) | (1+x)的自然对数,精确到非常小的x |
Math.expm1(x) | Math.log1p()的逆运算 |
Math.sign(x) | 对<,==或者>0的参数返回-1,0,1 |
Math.imul(x,y) | x*y,优化的32位整数乘法 |
Math.clz32(0xf) | 28,32位整数前导0的位数 (有点迷) |
Math.trunc(3.9) | 减去分数部分,得到整数,如果参数为正数,则 Math.trunc() 等效于 Math.floor(),否则 Math.trunc() 等效于 Math.ceil() |
Math.fround(x) | 舍入到最接近的32位浮点数 |
Math.sinh(x) | 双曲线正弦,类似还有Math.cosh(),Math.tanh() |
Math.asinh(x) | 双曲线反正弦,还有Math.acosh(),Math.atanh() |
Math.floor(),Math.ceil()与Math.trunc() 区别?
-Infinity ... ------+----|--+------+------+------+------+--|----+------ .... Infinity
在坐标轴上,Math.floor和Math.ceil都是向下或者向上(这里的向下或向上是对应坐标轴的向左或者向右)取整的,也就是说,在值为正数的时候Math.floor和Math.ceil能够正确的获得我们想要的值,而对于负数来说,例如-2.1,使用Math.floor方法时他会向下(向左)取整,最终得到-3,这与我们的意愿是违背的,而Math.trunc就很好的解决了这个问题,无论在正数还是负数上都能获取到正确的值,另外从性能上来说Math.trunc速度最快(具体原因不深入研究)。
JavaScript在最大数字溢出时并不会发生错误,而是产生一个Infinity或者-Infinity。在IEEE754中,有+0和-0之区分,当 下溢出发生在数值操作的结果比最小可表示数更接近0时,JavaScript会返回0,如果下溢出来自负数则返回-0,而+0和-0几乎完全无法区分(可通过Onject.is(+0,-0)来区分)。
在JavaScript中,Number.MIN_VALUE
和Number.MAX_VALUE
是预定义的常量,它们分别代表JavaScript中可以表示的最小和最大的数值。
Number.MIN_VALUE
是一个极小的正数,等于 5e-324。这是在JavaScript中可以表示的最小的非零数值。尝试除以它的一半(例如 Number.MIN_VALUE / 2
)会产生一个更小的数,但由于JavaScript中浮点数的精度限制,结果实际上会被四舍五入为0。
Number.MAX_VALUE
是一个非常大的数,等于 1.7976931348623157e+308。这是在JavaScript中可以表示的最大数值。任何大于此值的数字都将溢出,并且任何小于此值的负数将无法精确表示。
javascript
console.log(Number.MAX_VALUE / 2) // 8.988465674311579e+307 正常
console.log(-Number.MIN_VALUE / 2)) // -0 这也是上面所说的下溢出发生在数值操作的结果比最小可表示数更接近0时,JavaScript会返回0
注意!!!!
-0 与 +0 可不相同,在某些情况下他们会产生不同的结果,所以在一些框架中,会对这两个值进行处理判断。
例如:
1/-0 = -Infinity
1 / 0 = Infinity
这两个值完全不同!!!
关于NaN
在JavaScript中0做除数是允许的,他只会返回-Infinity或者+Infinity,而当0 / 0 时会返回NaN(not a number)表示不是一个Number,而无穷除无穷等无法转换为数值的操作都会返回NaN,NaN与任何数都不相等,连自己也不相等,而判断一个数是否是NaN则可以用下面两种方式:
- x != x:利用NaN与自身不相等的特性。
- Number.isNaN(x):通过API判断。
在全局作用域中,有个isNaN()
函数,与Number.isNaN()
类似,isNaN
在检测一个值是否为NaN之前,会首先尝试将这个值转换为数字类型;而Number.isNaN
则不会进行这种转换,它只判断传入的参数是否为数字类型。因此,当传入一个非数字类型的值时,isNaN
可能会返回true
(如果转换成功的话),而Number.isNaN
则只会返回false
(方便记忆:Number.isNaN()前提是Number类型,所以不用判断)。
Number对象
Number对象是JavaScript用于描述数值的内部对象,他有以下属性:
属性 | 描述 | 备注 |
---|---|---|
Number.POSITIVE_INFINITY | 表示Infinity | |
Number.NEGATIVE_INFINITY | 表示-Infinity | |
Number.MAX_VALUE | 最大有效数值1.7976931348623157e+308 | |
Number.MIN_VALUE | 最小正数有效值5e-324(小于这个数JavaScript会将其近似为0) | |
Number.parseInt(string,radix) | 与全局parseInt一样,将字符串解析为整数,radix表示进制数 | parseInt 会自动忽略字符串前面的空格,当字符串是以数字字符开头时它才会开始解析,并且忽略后面非数字部分,parseInt(' 123adsdas')=> 123,若加上进制数,则将字符串以对应的进制解析:parseInt(' ffggggaasd',16)=> 255 |
Number.parsFloat() | 与上类似 | |
Number.isNaN() | 判断是否为NaN | |
Number.isInteger() | 判断是否为整数 | |
Number.isSafeInterger() | 判断是否为安全的整数(是否在JavaScript安全数范围内) | |
Number.MAX_SAFE_INTEGER | 2**53-1 | |
Number.MIN_SAFE_INTEGER | -2**53-1 | |
Number.EPSLION() | 名为"机器精度" 数值与数值之间的最小差2**-52,JavaScript中只要两个数字之差小于这个属性值则认为两者相等 | |
Number.toExponential(x) | 把对象的值转换为指数计数法。 | |
Number.toFixed(x) | 把数字转换为字符串,结果的小数点后有指定位数的数字。 | 会根据其小数原始值(toPrecision方法的结果)进行四舍五入处理,由于一些小数的原始值会被计算机近似处理所以最终结果可能与你想的不一样,可见下介绍 |
Number.toLocaleString | 返回数字在特定语言环境下的表示字符串。 | |
Number.toPrecision(x) | 把数字格式化为指定的长度,指定有效位数的显示位数 | |
Number.toString() | 把数字转换为字符串,使用指定的基数。 | |
Number.valueOf() | 返回一个 Number 对象的基本数字值。 |
二进制浮点数摄入错误(小数精度丢失)
JavaScript的浮点格式只能表示有限个(18_437_736_874_454_810_627个),由于使用IEEE754浮点表示法,所以他可以 精确的表示1/2、1/4、1/8...1/1024等 2的n次方分之1的小数,但对于其他小数,会有一定的精度丢失问题:
ini
let x = 0.4 - 0.3 // 0.10000000000000003
let y = 0.2 - 0.1 // 0.1
console.log(x === y) //false
所以不能在小数运算中去比较其结果大小是否相等,一般得到的都不是准确的结果。 解决办法?: 可以使用第三方库(将数字变为字符串处理,精度提高但效率低下),或者将小数变为整数处理
再往下聊聊-计算机小数运算不精确原因
在计算机的小数计算中,存在三种不精确的过程
- 存储不精确:计算机使用有限的二进制位数来存储小数会导致舍入误差。例如,1/3(十进制中的无限循环小数)在二进制中也会变成一个无限循环小数,但计算机只能存储有限位数的近似值。(2的n次方分之1的小数存储是精确的)
- 计算不精确:连续的浮点数运算可能会导致精度丢失。简单的运算,如加法和减法,通常不会引入太大的误差,但复杂的运算,如除法和开方,可能会导致更大的误差。
- 显示不精确:当计算机将小数结果转换为可视化格式(浮点数的字符串表示),通常会进行四舍五入或 截断 以适应显示需求。这可能导致在显示时看到的结果与精确值不同。
为什么计算机不能精确的计算?说到底还是因为计算机底层是二进制而内存有限,所能表示的数是离散的,他只能给你做近似处理,不能完整的精确表示。可采用 定点算法 来提高精确度。
我们以实际代码为例:
javascript
1.55.toFixed(1) // '1.6'
2.55.toFixed(1) // '2.5'
1.55.toPrecision(20) //'1.5500000000000000444089209850062616169452667236328'
2.55.toPrecision(20) //'2.5499999999999998223643160599749535322189331054688'
// 所以你知道为什么了吗?
BigInt类型
BigInt类型是JavaScript在ES2020定义的一种新的类型,他是一种原始类型(也就是说你不能更改其值),这个类型的只要目的是为了表示64位整数,从而兼容其他语言和API,BigInt 类型可以表示任意大小的整数,没有理论上的上限。这主要取决于计算机内存的限制 ,bigInt类型写法是在一系列数字后面加个n,同样可以使用二进制,八进制或者十六进制来表示,他同样支持数值的基本运算,但是他不能被Math对象所操作,另外 不能在使用算术操作符时混用两种类型的操作数,但是你可以用比较运算符来进行两者的比较。
javascript
123n
0b1111n
0x78000n
1 < 2n
2 > 1n
0 == 0n
参考
- 《你不知道的JavaScript 中卷》
- 《JavaScript高级程序设计》
- 《JavaScript权威指南》