JavaScript 中有八种基本的数据类型(前七种为基本数据类型,也称为原始数据类型,后一种 Object 为复杂数据类型,也称为非原始数据类型或引用类型)。
number 用于任何类型的数字:整数或浮点数,在 ±(2(^53)-1) 范围内的整数。
bigint 用于任意长度的整数。
string 用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的字符类型。
boolean 用于 true 和 false。
null 用于未知的值 —— 只有一个 null 值的独立类型。
undefined 用于未定义的值 —— 只有一个 undefined 值的独立类型。
symbol 用于唯一的标识符。
Object 用于更复杂的数据结构。以下类型都是对象:
Function(函数)Array(数组)Date(日期)RegExp(正则表达式)JavaScript 包含两种不同类型的值:
栈内存和堆内存
当定义一个变量的时候,JavaScript 引擎会为变量分配两种内存:栈内存和堆内存。
静态值在编译阶段有固定的大小,静态值有:
Null、Undefined、Boolean、Number、String、Symbol、BigInt静态值有固定的大小,不能改变。JavaScript 引擎为它们分配一片固定的内存,并存储在栈上。例如:
let name = "John";
let age = 25;因为 name 和 age 都是原始值类型,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过程如下:
如下图:

因此,对两个变量的操作不会互相影响。
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}如下图所示:
修改前:

修改后:

总结
小练习
下面代码的输出结果是什么?
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 }。
原始类型和引用类型在内存中的分配与函数参数传递有联系。函数传参都是值传递,只不过根据值的类型不同有所区别。
由上可知:
这是一道经典的面试题:
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 位格式”有关。

在这个标准下:
1 - 1023 = -1022
2046 - 1023 = +1023此时,我们再来看 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。它会尽最大努力使数字符合所需的格式,但不幸的是,这种格式不够大到满足需求。
阅读量:2058
点赞量:0
收藏量:0