在JavaScript的世界里,数据类型是编程的基础。了解每一种数据类型的特性和使用场景,对于写出高效、可读性强的代码至关重要。本文将带你从基础开始,逐步深入JS的数据类型世界。
1. JS到底有多少种数据类型?
这个问题的答案并不唯一,它取决于你对数据类型的划分标准。一般来说,我们可以根据数据类型是否能够直接存储在栈内存中来区分它们:
- 简单数据类型(Primitive Types) :这些类型可以直接存储在栈内存中,包括
numeric(number + bigint)
、string
、boolean
、undefined
、null
和symbol
。当我们将这些类型的数据进行传递时,实际上是在传递它们的值副本,因此我们称这种传递方式为“按值传递”。 - 复杂数据类型(Object Types) :这类数据类型不能直接存储在栈内存中,而是存储在堆内存中,栈内存中保存的是指向堆内存中实际数据的指针,类型为
object
。因此,当我们传递对象时,实际上是传递了这个指针的副本,这意味着两个变量最终可能指向同一个对象,我们称之为“按引用传递”。
在一些讨论中,null
和undefined
可能会被视为特殊的情况,因为它们虽然被归类为简单数据类型,但有时也会被单独讨论。此外,随着ES6的引入,Symbol
类型成为了新的成员,而BigInt
则是在ES11中加入的新类型,用于表示任意大小的整数。
2. 代码解释
1. 简单数据类型(Primitive Types)
1.1 numeric :分为number和bigint,number包括普通的数字类型和大整数(bigInt),bigin表示大于 Number
能表示的整数范围的值。
let a = 5;
let b = a; // 按值传递,b 是 a 的一个副本
console.log(b); // 输出: 5
a = 10;
console.log(b); // 输出: 5,b 不受影响
let bigInt1 = 123456789012345678901234567890n;
let bigInt2 = BigInt(123456789012345678901234567890);
let number1 = 123456789012345678901234567890;
let number2 = 123456789012345678901234567890;
console.log(bigInt1); // 输出: 123456789012345678901234567890n
console.log(bigInt2); // 输出: 123456789012345678901234567890n
console.log(bigInt1 + bigInt2); // 输出: 246913578024691357802469135780n
console.log(number1 + number2); // 输出:2.4691357802469136e+29
console.log(typeof bigInt1); // 输出:number
console.log(typeof number1); // 输出:bigint
let a = 0.1;
let b = 0.2;
console.log(a + b); // 输出:0.30000000000000004
// 输出结果不准确是因为浮点数在计算机中是以二进制形式存储的,而某些十进制小数在二进制中是无限循环的。例如,十进制的 0.1 和 0.2 在二进制中无法精确表示,只能近似表示。这种近似表示在进行算术运算时会导致微小的误差积累。
这些值存储在栈中,直接存储数字本身。
1.2 string :字符串类型,表示文本数据。
let str1 = "Hello";
let str2 = str1;
console.log(str2); // 输出: Hello
str1 = "World";
console.log(str2); // 输出: Hello,str2 不受影响
console.log(typeof str1); // 输出:string
字符串是不可变的,一旦创建,就无法修改其中的字符。
1.3 boolean :布尔值,只有两个取值:true
或 false
。
let isTrue = true;
let isFalse = isTrue;
console.log(isFalse); // 输出: true
isTrue = false;
console.log(isFalse); // 输出: true,isFalse 不受影响
console.log(typeof isTrue); // 输出:boolean
1.4 undefined :表示变量已声明但未赋值时的默认值。
let undeclared;
console.log(undeclared); // 输出: undefined
console.log(typeof undeclared); // 输出: undefined
1.5 null :表示一个空值或不存在的对象,是一个可以赋给变量的特殊值。
let value = null;
console.log(value); // 输出: null
console.log(typeof value); // 输出: object (这是一个历史遗留问题(bug))
let a = null; // 栈内存
console.log(a);
let largerObject = {
data: new Array(1000000000).fill('a')// 堆内存
}
largerObject = null;// 释放内存 垃圾回收
1.6 symbol :表示唯一的标识符,常用于对象的属性键。
let sym1 = Symbol("key");
let sym2 = Symbol("key");
console.log(sym1 === sym2); // 输出: false,即使描述相同,符号也不同
let obj = {};
obj[sym1] = "value1";
obj[sym2] = "value2";
console.log(obj[sym1]); // 输出: value1
console.log(obj[sym2]); // 输出: value2
console.log(typeof sym1); // 输出:symbol
即使两个 Symbol 的描述相同,它们的值也是不相等的。
为什么简单数据类型叫“拷贝”?
由于简单数据类型的值是存储在栈内存中的,当你将一个简单数据类型的变量赋值给另一个变量时,JavaScript 实际上是创建了该值的一个副本,而不是复制它的内存地址。因此,两个变量的值是独立的,修改一个不会影响另一个。
2. 复杂数据类型(Object Types)
2.1 对象按引用传递
let obj1 = { name: "Alice" };
let obj2 = obj1; // 按引用传递,obj2 和 obj1 指向同一个对象
console.log(obj2.name); // 输出: Alice
obj1.name = "Bob";
console.log(obj2.name); // 输出: Bob,obj2 受影响
console.log(typeof obj1); // 输出:object
2.2 数组
let arr1 = [1, 2, 3];
let arr2 = arr1; // 按引用传递,arr2 和 arr1 指向同一个数组
console.log(arr2); // 输出: [1, 2, 3]
arr1.push(4);
console.log(arr2); // 输出: [1, 2, 3, 4],arr2 受影响
console.log(typeof arr1); // 输出:object
为什么复杂数据类型叫“引用”?
当你将一个复杂数据类型的变量赋值给另一个变量时,JavaScript 实际上并没有创建一个新的副本,而是将原始数据的引用(地址)赋给了新变量。因此,修改其中一个变量的属性或值会影响另一个变量,因为它们指向的是同一个内存地址。
3. 内存分配机制:栈与堆
为了更好地理解简单数据类型和复杂数据类型之间的区别,我们需要了解 JavaScript 中的内存分配机制:
- 栈内存:存储简单数据类型(如
number
,string
)的值。栈内存的分配和回收速度很快,因此这些值的赋值是“拷贝”而非“引用”。 - 堆内存:存储复杂数据类型(如对象、数组)的引用。由于堆内存的管理比较复杂,所以这些数据类型的赋值是“引用”而非“拷贝”。
结语
理解JS的数据类型不仅有助于避免常见的编程错误,还能让你的代码更加高效和优雅。无论是初学者还是有经验的开发者,持续深入学习和实践都是不可或缺的。希望本文能为你打开一扇通往更深层次JS世界的门。如果你有任何疑问或想要探讨更多相关话题,欢迎留言交流!