📕 重学JavaScript:数据是怎么存储的?
嗨,大家好!这里是道长王jj
~ 🎩🧙♂️
数据是用两种不同的方式来存储,一种叫做栈(stack),一种叫做堆(heap)。
栈是一种后进先出(LIFO)的数据结构,它可以快速地存取数据,但是空间有限。
堆是一种无序的数据结构,它可以存放大量的数据,但是速度较慢。
那么,JavaScript 中哪些数据类型用栈来存储,哪些用堆来存储呢?
💡 基本数据类型用【栈】存储,引用数据类型用【堆】存储
其实就是按照数据类型的复杂度来区分的。
基本数据类型(primitive types)比较简单,它们只占用固定大小的空间。😊
但是对象数据类型(object types)就比较复杂了,它们可以包含多个属性和方法。😅
❓ 数据存储过程中发生了什么?
当你给一个变量赋值一个基本类型的数据时,
这个数据会直接存储在栈中,而且每个变量都有自己独立的空间,互不影响。
比如,你有两个变量a和b,然后给它们赋值两个基本类型的数据,就像这样:
var a = 1;
var b = a;
这时候,栈中会有两个空间,分别存储a和b的值,就像这样:
栈 |
---|
b: 1 |
a: 1 |
这时候,a和b都是1,但是它们是两个不同的空间,互不影响。
所以,如果你改变了a的值,比如这样:
a = 2;
那么,栈中的空间会变成这样:
栈 |
---|
b: 1 |
a: 2 |
这时候,a变成了2,但是b还是1。这是因为a和b是两个不同的空间,互不影响。
当你给一个变量赋值一个对象类型的数据时,情况就不一样了。
这时候,这个数据会存储在堆中,并且栈中只存储一个指向堆中数据的引用(地址)。
而且如果有多个变量指向同一个对象类型的数据,那么它们就会共享同一个空间,相互影响。😭
比如,你有两个变量a和b,然后给它们赋值一个对象类型的数据,就像这样:
var a = {
name: "Tom" };
var b = a;
这时候,堆中会有一个空间,存储这个对象的属性和方法,就像这样:
堆 |
---|
{ name: “Tom” } |
而栈中会有两个空间,分别存储a和b的引用(地址),就像这样:
栈 |
---|
b: 堆地址 |
a: 堆地址 |
这时候,a和b都指向堆中的同一个对象。😮
所以,如果你改变了a的属性值,比如这样:
a.name = "Jerry";
那么,堆中的空间会变成这样:
堆 |
---|
{ name: “Jerry” } |
而栈中的空间不会变化。😊
这时候,a和b都指向堆中的同一个对象,并且它们的属性值都变成了"Jerry"。😭
这是因为a和b共享同一个空间,并且相互影响了。
所以,对于赋值操作,原始类型的数据直接完整地赋值【变量值】,对象数据类型的数据则是【复制引用地址】。
❓ 既然改变栈会引用同一份【堆空间地址】,为什么不全部用栈来保存?
因为栈的空间有限,而且栈的功能不只是保存数据,还有创建并切换函数执行上下文的功能。
如果全部用栈来保存数据,那就会出现几种可怕的情况:
栈的空间不够用
如果你要保存大量的数据,或者复杂的数据,那么栈的空间就会很快被占满,导致栈溢出(stack overflow)的错误。
比如,你想用一个递归函数来计算斐波那契数列(Fibonacci sequence)的第n项,就像这样:
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(1000));
这个函数看起来很简单,但是它有一个问题,就是它会产生大量的函数调用,每次调用都会把函数的执行上下文压入栈中。
如果你要计算较大的n,比如1000,那么栈的空间就会很快被占满,导致栈溢出的错误。😭
你可以用浏览器的开发者工具来运行试试看
RangeError: Maximum call stack size exceeded的错误,就是说栈的空间不够用了。
栈的速度变慢
栈是一种后进先出(LIFO)的数据结构,它只能从一端存取数据。
如果你要保存复杂的数据,或者嵌套的数据,那么你就需要多次进出栈,这样就会增加栈的操作次数和时间,导致栈的速度变慢。
比如,你想用一个函数来判断一个字符串是否是回文(palindrome),就像这样:
function isPalindrome(str) {
// 把字符串转换成数组
var arr = str.split("");
// 把数组反转
var reversedArr = arr.reverse();
// 把数组转换成字符串
var reversedStr = reversedArr.join("");
// 比较原字符串和反转后的字符串是否相等
return str === reversedStr;
}
console.log(isPalindrome("racecar")); // true
这个函数看起来也很简单,但是它有一个问题,就是它会产生大量的数组操作,每次操作都会把数组元素压入或弹出栈中。
如果你要判断较长的字符串,比如"racecar",那么栈的操作次数和时间就会增加,导致栈的速度变慢。
你可以用浏览器的开发者工具来查看这个函数的性能分析看看,是不是大部分时间都花在了数组操作上。
栈的功能受影响
栈不只是用来保存数据,还用来创建并切换函数执行上下文。
每当你调用一个函数时,就会把这个函数的执行上下文压入栈中;
每当你返回一个函数时,就会把这个函数的执行上下文弹出栈中。
如果你用栈来保存大量的数据,或者复杂的数据,那么就会占用栈的空间和时间,导致栈的功能受影响。
比如,你想用一个函数来实现柯里化(currying),就像这样:
function curry(fn) {
// 获取函数参数个数
var arity = fn.length;
// 返回一个柯里化后的函数
return function curried() {
// 获取当前函数的参数
var args = Array.prototype.slice.call(arguments);
// 判断当前参数是否足够
if (args.length >= arity) {
// 如果足够,就直接调用原函数
return fn.apply(null, args);
} else {
// 如果不足,就返回一个新的函数,等待接收剩余的参数
return function() {
// 获取新函数的参数
var newArgs = Array.prototype.slice.call(arguments);
// 合并新旧参数
var allArgs = args.concat(newArgs);
// 递归调用curried函数
return curried.apply(null, allArgs);
};
}
};
}
这个函数看起来很复杂,但是它有一个问题,就是它会产生大量的函数调用,每次调用都会把函数的执行上下文压入栈中。如果你要柯里化一个参数很多的函数,比如这样:
function add(a, b, c, d, e) {
return a + b + c + d + e;
}
var curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)(4)(5)); // 15
那么栈的空间和时间就会被占用很多,导致栈的功能受影响。😭
你可以用浏览器的开发者工具来查看这个函数的调用栈,你会发现每次都会把它的执行上下文压入栈中。
所以,为了避免这些问题,JS采用了两种不同的方式来存储数据,一种是用栈来存储基本类型的数据,一种是用堆来存储对象类型的数据。
这样可以充分利用栈和堆各自的优势,提高JS的性能和效率。😊
🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨