JavaScript类型全解析:从栈堆内存理解变量存储
从内存角度彻底搞懂JS类型
前言
你好👋最近在学JavaScript,发现它的类型系统看似简单,实则藏着不少"坑"。今天,我就把自己关于JS类型的学习笔记整理出来,和你一起分享探讨,如有理解不对的地方,欢迎你的指正!
一、JavaScript有哪些类型?
JavaScript中的类型可以分为两大类:
-
基本类型(原始类型) :值本身存储在栈内存中,按值访问。
-
引用类型:值存储在堆内存中,在栈内存中存储的是一个指向堆内存的地址(指针),按引用访问。
1. 基本类型
(1) number (数字类型)
let age = 25;
let price = 99.8;
console.log(typeof age); // "number"
注意: JS没有区分整数和浮点数,所有数字都是 number 类型。
(2) string (字符串类型)
let myname = "掘金";
let str = `Hello I am ${myname}`; // 模板字符串
console.log(typeof myname); // "string"
特点: 字符串是不可变的。一旦创建,就无法改变其中的字符。
(3) boolean (布尔类型)
let isLogin = true;
let isEmpty = false;
console.log(typeof isLogin); // "boolean"
(4) undefined
let a;
console.log(a); // undefined
console.log(typeof a); // "undefined"
特点: 表示变量已声明但未赋值。它是一个类型,也是这个类型唯一的值。
(5) null
let emptyValue = null;
console.log(emptyValue); // null
console.log(typeof emptyValue); // "object"
注意: 这是JS的一个历史悠久的Bug!null 本身是一个基本类型,但 typeof null 返回 "object"。所以,判断一个值是否为 null,最好直接用 == null。
console.log(emptyValue === null); // true
(6) symbol (ES6新增)
let s1 = Symbol('foo');
let s2 = Symbol('foo');
console.log(s1 === s2); // false,每个Symbol都是唯一的
console.log(typeof s1); // "symbol"
特点: 表示唯一的、不可变的值,常用于对象的唯一属性名。
(7) bigint (ES2020新增)
const bigNum = 9007199254740991n; // 在数字后面加 'n'
console.log(typeof bigNum); // "bigint"
特点: 用于表示大于 2^53 - 1 的整数,解决大数精度丢失问题。
2. 引用类型
通常我们说的引用类型,就是指 Object 类型,以及由它派生出来的其他子类型。
(1) object (对象)
let person = {
name: 'Jack',
age: 20
};
console.log(typeof person); // "object"
(2) array (数组)
let fruits = ['apple', 'banana'];
console.log(typeof fruits); // "object"
console.log(Array.isArray(fruits)); // true (判断数组的正确方法)
(3) function (函数)
function fn() {
console.log('Hello');
}
console.log(typeof fn); // "function"
注意: 函数在JS中是一等公民,它本质上是一种特殊的对象,所以 typeof 会返回 "function"。
二、核心原理:栈(Stack)与堆(Heap)的存储机制
要真正理解基本类型和引用类型的区别,我们需要了解它们在内存中是如何存储的。
内存的两个重要区域:
- 栈(Stack) :有序存储,空间较小,但访问速度快
- 堆(Heap) :无序存储,空间较大,但访问速度相对较慢
1. 基本类型存储在栈中
function stack() {
let a = 10; // 数字类型入栈
let b = 'hello'; // 字符串类型入栈
let c = true; // 布尔类型入栈
// 此时栈内存状态就如下图所示
console.log(a, b, c); // 10 'hello' true
}
stack();
内存中的情况:
特点: 直接存储值本身,大小固定,访问快速。
let a = 10;
let b = a; // 在栈中创建一个新的值 10 分配给 b
b = 20; // 修改 b 不会影响 a
console.log(a); // 10
console.log(b); // 20
注意: 变量赋值时,复制的是实际的值
示例:函数调用执行的过程。
var a = 1
function add(a) {
var b = 2
let c = 3
return a + b + c
}
add(a)
在执行这段代码之前,JS引擎会先创建一个全局执行上下文,包含所有已声明的函数与变量:
从图中可以看出,代码中的全局变量 a 及函数 add 保存在变量环境中。
执行上下文准备好后,开始执行全局代码,首先执行 a = 1 的赋值操作,
赋值完成后 a 的值由 undefined 变为 1,然后执行 add 函数,JavaScript 判断出这是一个函数调用,然后执行以下操作:
首先,从全局执行上下文中,取出 add 函数代码。
其次,对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码,并将执行上下文压入栈中。
最后,执行代码,返回结果,并将 add 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。
上面需要注意的是:函数(add)中存放在栈区的数据,在函数调用结束后,就已经自动的出栈,换句话说:栈中的变量在函数调用结束后,就会自动回收。所以,通常栈空间都不会设置太大。
2. 引用类型存储在堆中
堆数据结构是一种树状结构,它的存取数据的方式与书架与书非常相似。我们只需要知道书的名字就可以直接抽出书了,并不需要把上方的书全取出来。
在栈中存储不了的数据比如对象就会被存储在堆中,在栈中只是保留了对象在堆中的地址,也就是对象的引用 ,对于这种,我们把它叫做 按引用访问 。
示例:
var a = 1
function foo() {
var b = 2
var c = { name: 'an' } // 引用类型
}
foo()
堆空间通常很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。
特点: 在栈中存储的是指向堆内存的地址,实际的对象数据存储在堆中,堆内存空间动态分配,可以存储大量数据。
let obj1 = { count: 10 };
let obj2 = obj1; // 复制的是地址,不是实际对象!
obj2.count = 20; // 通过 obj2 的地址修改堆中的对象
console.log(obj1.count); // 20!obj1 也指向同一个对象
3.口袋与衣柜的比喻:为什么栈不能设计得很大
想象一下你的裤子口袋:
- 口袋很小,但拿东西特别快——伸手就能摸到
- 衣柜很大,但找东西比较慢——需要走过去翻找
栈就像裤子口袋,设计得小是为了存取速度极快。如果栈做得像衣柜那么大,每次找变量就像在乱糟糟的大衣柜里翻东西,速度就慢下来了。
简单说:对象太大,口袋(栈)装不下,只能在口袋里放个地址纸条,告诉你东西存在哪个衣柜(堆)里!
最后
理解JavaScript的类型和栈堆存储原理,是理解变量比较、函数参数传递、内存管理等高级概念的基础。从底层理解为什么引用类型会"共享"数据,为什么基本类型是"独立"的,这对写出正确的JS代码至关重要。
你在学习JS类型时还遇到过哪些困惑?欢迎在评论区一起交流!如果觉得这篇文章对你有帮助,请点个赞吧~ 这对我这个新人来说是莫大的鼓励!