轻松应对面试官对于JavaScript数据结构与内存管理的灵魂拷问

164 阅读9分钟

为什么你需要这篇指南?

面试的时候,我们可能会遇到这样的场景:面试官微笑着看着你,然后冷不丁的抛出一个看似简单实则深奥的问题:“你能解释一下JavaScript中的内存管理吗?”或者更直接一点:“JavaScript中有哪些常用的数据结构,它们的优缺点是什么?”这些问题听起来好像很简单,但如果你没有准备充分,很容易被问得一头雾水,甚至怀疑人生,结果与大厂失之交臂。

铁铁 Don't worry,你不是一个人在战斗!在这篇文章中,我们将一起探索JavaScript的数据结构和内存管理,让你在面试中不仅能从容应对,还能轻松反杀,让面试官刮目相看!

让JS的数据类型知识刻入DNA

在开始前,我浅浅问一句:铁铁,你晓得有数据类型分为多少种吗?哎~ 不论知不知道,我们来全部举例出来不就晓得了。

  • 简单数据类型:(Primitive)
    • JS 中的老牌成员:
      • number:用于表示数值,包括整数和浮点数。(有误差,精度丢失)
      • string:用于表示和操作文本数据。
      • boolean:用于表示逻辑值,只有两个可能的值:truefalse
      • undefined:表示一个未定义的值,通常用于声明但未赋值的变量或不存在的属性。
      • null:表示一个空值或不存在的对象。
    • es6后 的新增成员:
      • symbol:表示独一无二的标识符。
      • bigint:表示任意大小的整数,解决了 number 类型在处理大整数时的精度问题。
  • 复杂数据类型:
    • object(对象):表示复杂的数据结构,可以包含多个属性和方法,是所有引用类型的基类。

聪明的小伙伴就会说了,哎~ JS 中有 8 种数据类型。但是捏,大佬就会发话了,其实里面只有 7 种,因为numberbigint可以合称为numeric,所以我们可以说JS 中有 7 种数据类型

让我们有请今天的几位主角:

  • 1. symbol:

在上面我们知道symbol 用于表示独一无二的标识符。但是其也有一些特殊性质,每个 symbol 值都是唯一的,即使使用相同的描述符创建的 symbol 也是不同的。symbol 主要用于对象属性的键,以确保属性名的唯一性,从而避免命名冲突。例如:

console.log(Symbol('1') === Symbol('1')) //false 

我们创建了两个symbol,虽然他们都是'1',但是他们并不相等,因为他们具有唯一性.

  • 2. bigint:

由于传统的 number 类型在处理非常大的整数时可能会失去精度,因为 number 类型的最大安全整数是 2^53 - 1(即 9007199254740991)。超过这个范围的整数可能会导致精度丢失。而为了解决这个问题,在 ES2020 中引入了 bigint 类型。例如:

// 数字范围有限
// let num1 = 9999999999999999999999;
// let num2 = 8888888888888888888888;
// console.log(num1 + num2); 精度丢失

// bigint 大整数  解决精度丢失  以n结尾
let num1 = 9999999999999999999999n;
let num2 = 8888888888888888888888n;
console.log(num1 + num2);

但是不能解决number中浮点数计算的精度丢失哦,因为bigint只能处理整数.

  • 3.null:

null类型表示一个空值或不存在的对象,是一个可以赋给变量的特殊值,并且可以回收内存。哎~ 回收内存?null的这个功有点特殊哦,我们不妨来深挖一下。让我们来看个例子:

let a = null; // 存储在“栈内存”之中
console.log(a);

let largeObject = {
    data: new Array(100000000).fill('a') // .fill() 填充数组
} // 存储在“堆内存”之中,但是地址存放在“栈内存”之中

// 释放内存 垃圾回收(直接释放储存在堆内存中的内存)
largeObject = null;

这里我们接触到了两个概念,即栈内存(Stack Memory)和堆内存(Heap Memory),这也是今天的最终boss,让我们来深入了解一下.

JavaScript 的内存管理:

JavaScript 的内存管理是一个复杂但非常重要的主题。理解内存管理机制可以帮助你编写更高效、更稳定的代码,避免常见的内存泄漏问题。其已经成为了我们的必修课,那么我们就来详细了解其原理.

内存分配:

在 JavaScript 中,内存分配主要涉及栈内存(Stack Memory)和堆内存(Heap Memory)

栈内存(Stack Memory) :

栈内存是一种线性的内存结构,遵循后进先出(LIFO, Last In First Out)的原则,按照函数调用的顺序依次分配和释放内存,当函数执行完毕后,其在栈中分配的内存会自动释放。所以其主要用于存储简单的数据类型以及函数调用的上下文信息(如局部变量和函数参数)。但是由于栈内存的大小是固定的,通常较小,因此不适合存储大型数据结构。

  • 分配:当函数被调用时,一个新的栈帧会被创建,其中包含函数的局部变量和参数。
  • 释放:当函数执行完毕后,对应的栈帧会被销毁,释放其中的内存。

例如:

function add(a, b) {
  let result = a + b;
  return result;
}
let sum = add(10, 20);

在这个例子中:

  • add 函数被调用时,新的栈帧被创建,包含参数 a 和 b 以及局部变量 result
  • 当 add 函数执行完毕后,栈帧被销毁,ab 和 result 的内存被释放。
  • sum 在栈中分配内存,存储 add 函数的返回值。
堆内存(Heap Memory):

堆内存是一种非线性的内存结构,用于存储复杂的数据类型。其的分配和释放相对灵活,但速度较慢(动态管理内存),因为灵活这一特点,我们可以用其存储大型数据结构。但是其缺点也是比较明显的,堆内存的管理依赖于垃圾回收机制,需要开发者注意避免内存泄漏。

  • 分配:当创建一个对象或数组时,内存会在堆中分配。
  • 释放:当对象或数组不再被引用时,垃圾回收机制会释放其占用的内存。 例如:
let largeObject = {
    data: new Array(100000000).fill('a') // .fill() 填充数组
} // 存储在“堆内存”之中,地址存放在“栈内存”之中

// 释放内存 垃圾回收(直接释放储存在堆内存中的内存)
largeObject = null;

在这个例子中:

  • data 是在堆内存中分配的,因为无法在栈内存中存储这么大的数组,所以我们会在栈内存中存储堆内存的地址,方便访问。
  • 栈内存中只存储了指向这些对象的引用(指针)。
  • 最后利用垃圾回收机制,用null来释放内存。

为了能充分理解栈与堆的合作,我们来讲述一个更直观的例子:

let obj = {
    name : "西西里",
    job : "前端开发工程师",
    company : "无"
}
// 拷贝赋值
let a = 1;
let b = a;
b = 3;

// 引用式赋值
let obj2 = obj;

obj2.name = "东东里";
console.log(a,b);
console.log(obj,obj2);

image.png

发生了一个严重的事情,那就是我们obj中的name怎么变成了和obj2一样,但是我们并没有对obj进行修改呀,这是因为在对obj2进行赋值时,在栈内存调用的是我们储存复杂数据类型的内存位置的地址,即会查找到堆内存中,而obj2 = obj这一操作是将obj2在栈内存中存储的地址也指向obj存储的地址,所以在对obj2进行修改后,调用obj也会发生改变,不妨结合图解一起理解注释中的 拷贝赋值 和 引用赋值。 9d8add33a34c020d88067ccb03ad72ac.png

lQLPJwolXX0xi1XNAxbNA6GwcTMvgUKnqsAHJT4pL__-AA_929_790.png


现在是不是对这些数据类型和内存管理有了一定了解呢,当然其中的奥义还有许多,读者可以自行去寻找。

而在最后,我们再介绍一个用于查询数据类型的类型运算符。

收下吧,我最后的知识点:(介绍 typeof)

在 JavaScript 中有多种类型,那么为了能获得变量的确切类型,我们可以使用typeof这一运算符,话不多说,上例子:

let a = 1;
// typeof JS 类型运算符
// console.log(typeof a, typeof(a))
console.log(typeof a, + '1');

console.log(typeof "hello");
console.log(typeof true);
console.log(typeof undefined);
console.log(typeof Symbol('hello'));
console.log(typeof 1223n);
console.log(typeof null);

console.log(typeof function() {});

我们可以得到以下结果: image.png

那么眼尖的小伙伴就会发现了,这这这给我null干哪去了,这还是 JS 简单数据类型吗?

哎~ 这里就不得不提一个小小的bug了,typeof 除了null之外的primitive 类型,都可以得到正确的结果,但是null的结果是object,这是被夺舍了吗?

其实这是当初 JS 设计时的bug,要知道 JavaScript 是在 1995 年由 Netscape 公司的 Brendan Eich 在短短 10 天内设计完成。当时的 JavaScript 实现是基于 C 和 C++ 的,而这些语言中的 NULL 通常被定义为 (void*)0,即一个指向空地址的指针。但是在 JavaScript 早期设计时将 null 视为一个特殊的对象(object)引用,typeof null 返回 "object" 的行为在 JavaScript 的早期版本中就已经确定。为了保持向后兼容,这个行为在后续的 JavaScript 版本中没有改变,所以才会发生这个所谓的“bug”。

总结

经过这一番深入浅出的讲解,相信你现在已经对 JavaScript 的数据类型和内存管理有了深刻的理解。希望这篇文章能为你查缺补漏,让你在 JS 的“肌肉”更加强大,轻松应对那些看似“灵魂拷问”的问题。

---欢迎各位点赞、收藏、关注,如果觉得有收获或者需要改进的地方,希望评论在下方,不定期更新,都看到这里了,真的忍心不点个赞吗~~~

0bae-hcffhsw0416753.gif