第二章:JavaScript数据类型—上-灵析社区

懒人学前端

一、JavaScript 数据类型有哪些?

JavaScript 中有八种基本的数据类型(前七种为基本数据类型,也称为原始数据类型,后一种 Object 为复杂数据类型,也称为非原始数据类型或引用类型)。

  • 其中原始数据类型:

number 用于任何类型的数字:整数或浮点数,在 ±(2(^53)-1) 范围内的整数。

bigint 用于任意长度的整数。

string 用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的字符类型。

boolean 用于 true 和 false。

null 用于未知的值 —— 只有一个 null 值的独立类型。

undefined 用于未定义的值 —— 只有一个 undefined 值的独立类型。

symbol 用于唯一的标识符。

  • 以及一种非原始数据类型:

Object 用于更复杂的数据结构。以下类型都是对象:

  • Function(函数)
  • Array(数组)
  • Date(日期)
  • RegExp(正则表达式)

二、原始数据类型和引用数据类型的区别?

JavaScript 包含两种不同类型的值:

  • 原始数据类型(Primitive values)
  • 引用数据类型(Reference values)

栈内存和堆内存

当定义一个变量的时候,JavaScript 引擎会为变量分配两种内存:栈内存和堆内存。

静态值在编译阶段有固定的大小,静态值有:

  • 原始值:NullUndefinedBooleanNumberStringSymbolBigInt
  • 引用值:是对象的引用。

静态值有固定的大小,不能改变。JavaScript 引擎为它们分配一片固定的内存,并存储在栈上。例如:

let name = "John";
let age = 25;

因为 nameage 都是原始值类型,JavaScript 引擎将它们存储在栈上,如下图所示:

JavaScript 将对象(Object) 存储在堆(heap)上。

let person = {
    name: "John",
    age: 25
};

内存如下图:

JavaScript 引擎在堆内存上创建了一个新的对象,同时它和栈内存上的 person 变量连接。因此,我们说 person 变量是对象的引用。

动态属性

一个引用值允许我们添加、修改和删除属性,例如:

let person = {
    name: "John",
    age: 25
};

// 添加属性 ssn 
person.ssn = "123-45";

// 修改 name
person.name = "John Doe";

// 删除属性 age 
delete person.age;

console.log(person); // {name: 'John Doe', ssn: '123-45'}

JavaScript 也允许在原始值上添加属性,但这个属性不会起作用。

let name = "John";
name.alias = "Knight";
console.log(name.alias); // undefined

复制值

原始值

对于原始值来说,JavaScript 引擎创建一个值的副本,并将值赋给新的变量。

let age = 25;
let newAge = age; 
console.log(age, newAge); // 25 25

过程如下:

  • 首先,声明一个变量 age,并将 25 赋值给它。
  • 其次,声明另一个新的变量 newAge,将 age 赋值给 newAge,JavaScript 引擎将 25 复制了一份,给了新变量。

如下图:

因此,对两个变量的操作不会互相影响。

let age = 25;
let newAge = age;

newAge = newAge + 1;
console.log(age, newAge); // 25 26

如下图:

引用值

对于引用值来说,复制的值指向的是同一个对象,因此操作的是也是同一个对象。

当我们将一个引用值从一个变量赋值给另一个变量,JavaScript 引擎创建一个引用,因此两个变量都是指向堆内存中的同一个对象。意味着,你修改其中一个,另一个也会被修改。

let person = {
    name: "John",
    age: 25
}

let member = person;
member.age = 26;

console.log(person); // {name: 'John', age: 26}
console.log(member); // {name: 'John', age: 26}

如下图所示:

修改前:

修改后:

总结

  • JavaScript 有两种类型的值:原始值和引用值
  • 引用类型的值可以对它的属性做增删改查,原始值不行
  • 从一个变量复制原始值到另一个变量,会创建一个独立的值的备份,意味着修改一个变量不会影响到另一个变量
  • 从一个变量复制引用值到另一个变量,两个变量会指向同一个对象,意味着通过一个变量修改对象将会影响到另一个对象。

小练习

下面代码的输出结果是什么?

let person = {
    name: "John",
    age: 25
}

function increaseAge(obj) {
    obj.age += 1;
    obj = {name: "Jame", age: 22}
    console.log(obj); // 1
}

increaseAge(person);
console.log(person); // 2

答案:1 处是:{ name: "Jame", age: 22 }, 2 处是 { name: 'John', age: 26 }。两个变量的内存示意图如下:

实际上, obj = {name: "Jame", age: 22} 使得 obj 重新指向了一个新的引用的,对象在堆内存中重新分配一块内存,并让 obj 指向它。 因此 console.log(person) 的结果是 { name: "Jame", age: 22 }

原始类型和引用类型在内存中的分配与函数参数传递有联系。函数传参都是值传递,只不过根据值的类型不同有所区别。

由上可知:

  • JavaScript 实参都是传值
  • 函数实参会在函数中创建新的局部变量。

三、为什么 0.1 + 0.2 !== 0.3 ?

这是一道经典的面试题:

0.1 + 0.2 === 0.3; // false

这里涉及到 JavaScript 中的数字类型。下面是 Number 的定义:

MDN 中对 Number 的定义如下:

根据语言规范,JavaScript 采用“遵循 IEEE 754 标准的双精度 64 位格式”("double-precision 64-bit format IEEE 754 values")表示数字。

为什么会这样?

简单地说,0.1 和 0.2 的二进制表示形式是不准确的,所以它们相加时,结果不是精确的 0.3, 而是非常接近的值:0.30000000000000004。

这是和 JavaScript 采用“遵循 IEEE 754 标准的双精度 64 位格式”有关。

  • sign bit(符号): 用来表示正负号
  • exponent(指数): 用来表示次方数
  • mantissa(尾数):用来表示精确度

在这个标准下:

  • 1 位存储符号(Sign),0 表示正数, 1 表示负数。
  • 用 11 位存储指数,指数必须是“有符号”的值,这里使用了偏差指数,即存储 E + bias 的值。对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。11 位无符号整数的值的范围是 0 到 2^11(2047),由于全 0 和 全 1 的指数值是为特殊数字保留的,所以可用的指数是从 1 到 2046。减去指数偏差值 1023, 就能得到指数的实际范围,即从 -1022 到 +1023。
1 - 1023 = -1022
2046 - 1023 = +1023
  • 用 52 位存储 Fraction。

此时,我们再来看 0.1 + 0.2 的转换过程,举个例子,拿 0.1 来看:

0.1 对应的二进制是 1 * 2^-4 * 1.1001100110011……

符号位:0

E + bais: -4 + 1023 = 1019

Fraction: 1001100110011……

对应的 64 位完整表示如下图:

同理,0.2 的完整表示是:

所以,当 0.1 存下来的时候,就发生了精度丢失,当我们用浮点数进行运算的时候,使用的其实是精度丢失后的数。

当我们对两个数字求和时,它们的“精度损失”会叠加起来。这就是为什么 0.1 + 0.2 不等于 0.3。

如何解决?

方法一:toFixed(n)

我们可以借助方法 toFixed(n) 对结果进行舍入。

let sum = 0.1 + 0.2;
alert(sum.toFixed(2)); // 0.30

注意:toFixed 总是返回一个字符串。我们可以使用一元加号将其强制转换为一个数字:

let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // "0.30"

方法二:将数字临时乘以 100(或更大的数字),将其转换为整数,进行数学运算,然后再除回。

alert( (0.1 * 100 + 0.2 * 100) / 100 ); // 0.3

方法三:使用 Number.EPSILON。如果两个数的精度损失在允许范围内,则可以认为两个数是相等的。

Number.EPSILON 属性表示 1 与 Number 可表示的大于 1 的最小的浮点数之间的差值。

function numbersCloseEnoughToEqual(n1, n2) {
    return Math.abs(n1 - n2) < Number.EPSILON;
}

let a = 0.1 + 0.2;
let b = 0.3;

numbersCloseEnoughToEqual(a, b); // true
numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false

小练习

下面代码的打印结果是什么?

console.log(9999999999999999); // 16位

答案:输出结果 10000000000000000(17位)。

这也是因为精度损失。有 64 位来表示该数字,其中 52 位可用于存储数字,但这还不够。所以最不重要的数字就消失了。

JavaScript 不会在此类事件中触发 error。它会尽最大努力使数字符合所需的格式,但不幸的是,这种格式不够大到满足需求。

阅读量:2010

点赞量:0

收藏量:0