《You Dont Know JS中卷》(一,二) ---类型,值

167 阅读7分钟

类型

一些老生常谈的变量类型的介绍这里就不赘述了,需要有一点注意的就是: JS中变量是没有类型的,他们持有的值才有

对js中几个最基本的内置类型: 数组,字符串,数字,特殊值进行深入理解

数组

数组的一些常规特性这里也不赘述了,讲两种不太常见的情况

稀疏数组

含有空缺的数组,创建一个稀疏数组:

const arr = []
arr[0] = 1;
arr[3] = 4;
console.log(arr.length) // 4
console.log(typeof arr[2]); // undefined
arr.forEach((item) => {
    console.log(item); // 1,4
});
console.log(arr) //  [1, empty × 2, 3]

打印这个数组回发现,空缺的位置使用empty进行填补,并且他的值是undefined,遍历时与直接将undefined赋值给元素不同,empty遍历时不会遍历

类数组

具有数组索引,并且具有length属性,但不具有数组的其他特征的对象,常见的有:

  • document.querySelector等方法获取的NodeList
  • arguments对象

常见的类数组转换为数组的方法:

  • 解构: [...arguments]
  • call: [].slice.call(arguments)
  • Array.from: Array.from(arguments)

字符串

js中的字符串是不可变的,因此无法通过下标修改某个字符

let str = 'abc'
str[0] = 'hhhh'
console.log(str) //abc

同时也无法借用数组的可变更成员函数: splice,reverse等,因为这些方法试图修改字符串

数字

较小数值

0.1+0.2 === 0.3 // false很多同学可能都见过这种情况,不仅是js拥有,所有遵循IEEE 745规范的语言都是如此,简单来说浮点数中的0.1,0.2并不精确,他们相加为0.30000000000000004,所以判断结果为false

出现误差的原因:

IEEE 745规范规范中,十进制小数转换成二进制按照以下规律:小数部分乘2取整数,若相乘之后的小数部分不为0,则继续乘以2,直到小数部分为0,将取出的整数正向排序
如: 0.1转换为二进制
0.1 * 2 = 0.2 --------------- 取整数 0,小数 0.2
0.2 * 2 = 0.4 --------------- 取整数 0,小数 0.4
0.4 * 2 = 0.8 --------------- 取整数 0,小数 0.8
0.8 * 2 = 1.6 --------------- 取整数 1,小数 0.6
0.6 * 2 = 1.2 --------------- 取整数 1,小数 0.2
0.2 * 2 = 0.4 --------------- 取整数 0,小数 0.4
0.4 * 2 = 0.8 --------------- 取整数 0,小数 0.8
0.8 * 2 = 1.6 --------------- 取整数 1,小数 0.6
0.6 * 2 = 1.2 --------------- 取整数 1,小数 0.2
...
​
最后将取出的整数正向排列: 0.0001100110011001100110011001100110011001100110011001101...

上述例子可以看到,整数部分一直不会是0,所以会无限循环下去,后面的二进制只能存储52位,这又是为什么呢?下面会给你答案

IEEE 754

二进制浮点数算数标准的简称,其最常用的两种浮点数表示方式为: 单精度浮点数(32位),双精度浮点数(64位),这里重点介绍以下js中使用的双精度浮点数:

64Bits分为以下三个部分:

  1. 符号部分(S): 占1个bit
  2. 指数部分(E):表示次方数,占11个bit
  3. 尾数: 用来表示精度的部分(就是值的部分),占52个bit,

综上所述,0.1转换为二进制,尾数会是无限循环,双精度浮点数最多存储52个尾数.这也就导致0.1转换为二进制之后就已经不是数学意义上的0.1了

误差范围

那么如何判断这种理应相等但由于精确度引起的不相等情况呢?

我们可以设定一个误差范围,只要小于这个误差范围的数值,就可以看作是相等的,js中这个值通常是2^-52存储在属性Number.EPSILON,

可以直接使用:

function numCloseEnoughEqual(a,b){
  return Math.abs(a-b) < Number.EPSILON  
}

整数和浮点数的安全范围

浮点数最大值为: 1.798e+308定义在Number.MAX_VALUE

浮点数最小值为: 5e-325定义在Number.MIN_VALUE

整数最小值为: 定义在Number.MIN_SAFE_INTEGER,中

整数的最大值为: 定义在Number.MAX_SAFE_INTEGER

整数的检测

使用Number.isInteger可以检测一个数是否是整数

使用Number.isSafeInteger可以检测一个数是否是安全整数(小于等于Number.MAX_SAFE_INTEGER)

特殊值

null和undefined类型比较特殊,每个类型分别只有一个值: nullundefined

  • undefined: 表示没有值
  • null: 表示值为空

void运算符

undefined是一个内置标识符,但它可以重新被定义:

const undefined = 9;
接下来与undefined比较相当于和9比较

对此我们可以使用void来代替undefined进行比较运算,防止undefined被提前篡改导致比较不准确

void __没有返回值,因此范围结果是undefined

一般情况下我们使用void 0来获得undefined(源于C语言)

特殊的数字

  1. 不是数字的数字

    数学运算时,操作数不是数值类型,则会返回NaN,意思是not a number,一个警戒值,表示数学运算没有成功,这是失败后返回的结果

    • NaN也是数值类型
    • 唯一一个自身不等于自身的值,判断一个值是否是NaN应使用 Number.isNaN方法
  2. 无穷数

    正无穷: Infinity,负无穷:-Infinity:

    • 1/0 等于Infinity,任意一个操作数为负数则结果为-Infinity

    • 计算结果溢出时,js使用就近原则取值

      Number.MAX_VALUE + Math.pow(2,970) // Infinity
      Number.MAX_VALUE + Math.pow(2,969) // 1.7976.....
      

      出现上述结果的原因就是: 前者结果更接近Infinity因此"向上取整",后者更接近Number.MAX_VALUE,所以"向下取整"

    • 无穷除以无穷不等于1而等于NaN,这是因为无穷除以无穷在js中属于未定义的操作,所以结果为NaN

  3. 零值

    js中有+0(0),-0之分,有以下几个特点:

    • +0 === -0 => true

    • 0/-3=-0,0*-3=-0,加减运算不会得到-0

    • 数字-0转换为字符串得到的是字符串 "0",字符串"-0"转换为数字得到的是-0

    • 判断是否为-0

      function isNefZero(num){
          num = Number(num)
          return num === 0 && 1 / num === -Infinity
      }
      
    • 为什么需要-0

      在一些应用程序中数字的符号位可能用来代表其他信息(如移动方向),物体在0位置的移动方向就可以通过符号位来确定

特殊的等式

上文我们提到过由于0 === -0,我们需要polyfill一个方法来进行-0的判断,同样的对于NaN我们也需要专用的Number.isNaN方法进行判断.对于这两个比较特殊的等式,es6之后新加入了Object.is方法来判断两个值是否绝对相等,可以用来处理上述两种特殊情况

let a = 0 / -1
Object.is(a,-0) // true
let b = -1 * 's'
Object.is(b,NaN) // true

除了对特殊值进行比较之外,尽量使用===,其相率更高,Object.is主要用来处理哪些特殊的相等比较

值和引用

js中值进行复制或者传递的时候,我们无法决定使用值复制还是引用复制,这是由值类型决定的:

  • 基本数据类型通过值复制来进行传递或赋值: Number,String,undefined,null,Symbol,Boolean
  • 复杂数据类型通过引用复制来进行传递或赋值: Object

主要原因是: js内存有堆和栈,栈是一个连续的存储空间,主要存储基本数据类型,堆内存主要存储复杂数据类型如图:

  • 对于基本数据类型: 值直接存放在栈中
  • 对于复杂数据类型: 栈中存储复杂数据类型的引用,具体的值存储在堆内存中

image.png

因此复杂数据类型复制的时候实际复制的是栈中的引用,基本数据类型复制的时候复制的是栈中正儿八经的值,对于复杂数据类型,如果值更改了,所有引用这个值的变量都会更改:

let obj1 = { a: 2}
​
function fn(o){
    o.a = 3
    reutrn o
}
​
let obj2 = fn(obj1)
console.log(obj1.a,obj2.a) // 3,3 全部被修改了,原因就是复杂数据类型使用`引用复制`进行传递,obj1做为参数赋值,导致o也成为了obj1的引用,修改o等于修改obj1