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。它会尽最大努力使数字符合所需的格式,但不幸的是,这种格式不够大到满足需求。
阅读量:2010
点赞量:0
收藏量:0