Javascript 中的内存引用

Javascript 中的内存引用

Photo by 哈里森布罗德本特 on 不飞溅

在本文中,我将尝试通过一个示例练习来解释 Javascript 中的内存引用是如何处理的,我认为这可以更好地展示它的理解如何在我们的日常工作中真正有帮助。

我从 CodeSignal 进行了这个练习,它的解决方案虽然不是最直接或最简单的,但专门用于解释这个主题。

所以,问题是这样的:

我们需要编写一个函数来反转输入字符串中(可能是嵌套的)括号之间的字符。一些例子可能是:

  • '(foobar)' → 结果为 'raboof'
  • 'foo(bar(baz)se)bl(im)' → 结果为 'fooesbazrabblmi'
  • 'foo(bar)baz(blim)' → 结果为 fooabbazmilb

现在,我们将采用以下方法:

我们想分别创建一个包含每个字母的数组,并为每个括号嵌套一个额外的数组,因此前面的示例如下所示:

这样,我们可以在“完成”或“关闭”它们时反转每个数组以获得所需的结果。因此,第二个示例中的顺序为:

  • ['b','a','z']
  • ['b', 'a', 'r', ['b', 'a', 'z'], 'e', 's']
  • ['我是']

我希望你开始明白我的意图。

为了做到这一点,我们先来回顾一下内存引用的基础知识。

内存参考

在 Javascript 中,当我们为变量赋值时,我们可以通过两种不同的方式来做到这一点:

  1. 直接分配一个不可变值,这发生在所有原始数据类型(数字、字符串、布尔值、符号、bigint、未定义和空值)中
  2. 或者保存一个 引用内存中的特定位置 , 与对象。

介绍

以下部分部分摘自 这里 ,因此请随时咨询以获取更多信息。

为了进一步深入理论,JavaScript 引擎将数据存储在两个地方: .堆栈用于存储原语( 或静态 数据),其大小是已知的 编译时间 , 并且, 因为 他们的价值观不会改变 (我们将在稍后讨论),a 固定数量的内存 将为每种数据类型分配,过程称为 静态内存分配 .

另一方面,Javascript 使用 储藏 物体, 与堆栈不同的是,引擎 根据需要分配内存, 因为它们的大小只能在 运行。

所有变量首先指向堆栈。如果它是非原始值,则堆栈包含 参考 到对象在堆中的位置,因为堆没有以任何方式排序。

Stack and heap pointers

原语

什么时候我们 分配 Javascript中的东西,我们说我们是 捆绑 一个变量到一个特定的值(原始的或非原始的)。所以,这些“绑定”代表了一个 指针 保存在内存中的一堆位。让我们考虑一下 让变量 = 5 现在,我们可以重新分配(因为我们正在使用 ) 它的值,但我们并没有将那个 5 “转换”为,比如说,一个 6,而是保存到一组不同的位中,加起来为 6。如果我们创建另一个变量 常量变量2 = 6 “具有”相同的值,我们将使它指向与前一个相同的位集,这就是为什么在这一点上: 变量 === 变量2 会产生 真的。 就是这样 === 确实,它根据变量或值在内存中的特定位置比较变量或值。

所有原语都是 不可变的。 让我们考虑这个新变量 让 str = '猫' .如果我们尝试 变异 它的值,Javascript 会简单地忽略它:

 让 str = '猫'  
 str[0] = 'b'  
 console.log(str) // 产生 'cat',而不是 'bat'

每次我们 重新分配 一些东西,不要认为它改变了以前的值(或位),但是 创建一个新的。

对象

对象的存储方式不同。如前所述,它们的绑定点 **** 到内存中保存的一堆位,但这次它们不代表值,而是 找到它们的地址 (某一些 间接寻址 )。

在这里,有两个对同一个对象的引用和两个不同的对象包含相同的属性是有区别的。每次我们创建一个全新的对象时,Javascript 都会分离一个 新的 大块内存供其使用;这就是为什么如果我们这样做:

 常量 obj1 = { 道具:123 }  
 常量 obj2 = { 道具:123 }  
 console.log(obj1 === obj2) // 假

我们没有创建相同的对象,因为它们的内存位置不同。原语不是这种情况,因为正如我们之前所说,它们的大小在执行之前是已知的,因此 Javascript 可以完美地分配正确的内存量并将该确切的位块用于其他变量(具有相同的值)。

将变量分配给另一个变量时,两者的行为相同:

 常量 obj1 = { 道具:123 }  
 常量 obj2 = obj1  
 console.log(obj1 === obj2) // 真  
 常量基元1 = 123  
 常量原始 2 = 原始 1  
 console.log(primitive1 === original2) // 真

因为我们在这里抓住 精确的 块在别处使用。换一种说法,就像每一对 ** __ 坚持相同的价值观** ,任何其他绑定或属性也可以做的事情。

这里的一个主要区别是对象实际上是 可变的 .如果是 对象1 对象2 ,我们以某种方式将它们联系在一起。如果他们中的任何一个修改了那个地方/对象内的东西 {道具:123} , 因为他们都 观点 对它,另一个将是 看到相同的修改 .基本上,通过 对象1 我们正在创建并保存指向所述对象的指针,同时使用 对象2 我们只是借用它,这可以被认为是 两者都具有相同的指针,而不是值 .除非重新分配,否则它们将始终指向完全相同的内存引用。所以:

 常量 obj1 = { 道具:123 }  
 常量 obj2 = obj1  
 obj2.prop = 456  
 console.log(obj1) // { prop: 456 }  
 obj1.prop = 789  
 console.log(obj2) // { prop: 789 }

对于一个对象,花括号只是一个将一堆属性组合在一起的外壳,它不会封闭或阻止来自外部代理的任何修改。用这个例子思考一下:

 常量 obj1 = { prop1: 123 }  
 常量 obj2 = { prop2: obj1 }  
 obj2.prop2.prop1 = 456  
 console.log(obj1) // { prop1: 456 } obj2.prop2.anotherProp = 789  
 console.log(obj1) // { prop1: 456, anotherProp: 789 } 删除 obj2.prop2.anotherProp  
 console.log(obj1) // { prop1: 456 }

在这种情况下,我们有一个行为与对象完全一样的属性,因为嗯……确实如此。所以两个变量从哪里共享相同的内存引用并不重要,当修改一个时,我们将修改另一个。

如果我们也这样做,但这次使用原语,它不会产生相同的结果:

 常量 obj1 = { prop1: 123 };  
 常量 obj2 = { prop2: obj1.prop1 };  
 obj2.prop2 = 456;  
 控制台.log(obj1); // { prop1: 123 }

这是因为我们不是在变异,而是在重新分配;放手去把握另一个价值。所以,基本上,如果我们想要链接两个原始值,我们需要将它们包装在对象中。前面的示例类似于以下示例:

 常量 obj1 = { prop1: 123 };  
 让 obj2 = obj1  
 obj2 = { prop2: 456 };  
 控制台.log(obj1); // { prop1: 123 }

运动分辨率

最后......我们现在可以开始思考我之前提到的练习,如果你忘记了前提,请花一分钟时间阅读它......。

所以,要解决这个问题,首先我们必须考虑我们将如何完成上述实施。首先,我们必须在任何给定时刻跟踪打开括号的数量:

二、我们想每次遇到都新建一个数组 '(', 视作我们的活期存款,填写活期信件。

当前一个数组仍然“打开”(即嵌套数组)时,需要创建附加数组会出现问题,因为我们需要返回并在关闭它们的子数组时继续填充所有以前的数组。幸运的是,我们拥有我们所需要的: ,又名 块历史 大批。而且由于我们永远不会有(如练习所述)非闭合括号集,这将完美地工作。因此,每当我们需要创建一个数组时,我们都会将其推送到我们的历史记录中。

但这里有一个棘手的部分:当回到前一个数组时,我们希望它也 对我们刚刚离开的那个进行了修改 .为了解决这个问题,我们将把每个新数组推到两个不同的地方: 历史数组本身和前一个历史元素 .和我们 做到这一点 精确的 他们两个的内存引用相同( 新块 )

这样,因为我们是递归地执行这个过程,如果我们修改最后一个元素,它在所有先前块中的实例也会改变。我们正在建造一个俄罗斯套娃,但所有级别都已填满:

Items of chunkHistory array

将颜色视为相同的内存参考。

考虑到这一点,我们历史上的第一个数组将保存我们的最终字符串,因为它具有 全部 对其内部数组的引用,因此它会受到历史中任何元素的所有修改。

考虑到这一切,即使它们看起来是一样的,我们也不能如下进行:

 chunkHistory.at(-1).push([])  
 chunkHistory.push([])

因为每次我们都会采取 新的 放在内存中,修改一个不会改变另一个。

但是我们在这里遗漏了一些东西……将字母添加到相应的数组中。因此,我们只想推送字母(而不是任何括号):

而且因为 chunkHistory.at(-1) 永远是我们最嵌套、最不完整的数组 ,我们知道我们会将字母推入正确的层。

现在我们必须处理关闭数组。为此,我们必须考虑以下几点:

因为此时我们将离开我们的数组并且不再修改它,现在是做练习要求我们做的事情的好时机: 扭转它 .这里的问题是因为我们在很多地方同时使用相同的内存引用,我们不能重新分配或创建新的数组,而是修改现有的,即 进行不纯的修改:

 const reversedChunk = chunkHistory.at(-1).flat(chunkHistory.length).reverse(); chunkHistory.at(-1).length = 0; chunkHistory.at(-1).push(...reversedChunk); 块历史.pop();

在这里,我们正在展平所有层 chunkHistory.at(-1) 因为它里面可能包含另一个(已经反转的)数组。其次,要对最后一个块进行不纯修改,我们首先需要清空它,然后用反转的值填充它。最后,我们希望前一个块成为我们历史中的最后一项(当前项)。

因为我们无法进行所有这些修改,所以我们首先创建一个具有所需状态(reversedChunk)的数组,然后他们修改我们当前的数组以保持相同的值。我们的最终函数如下所示:

为了更好地理解这是如何工作的,这里是 chunkHistory 的值 每一步 我们的 第二个例子

  • 'foo(bar(baz)se)bl(im)' → 结果为 'fooesbazrabblmi'

注意每个新字母是如何被推入的 全部 要点 块历史 同时具有相同的引用。因此,例如,当 'baz' 数组出现在第四行(它最初创建为空)时,要填充它和所有其他实例,只需 chunkHistory.at(-1).push(symbol) .当然,这可以通过任何其他实例来完成,但我们使用的那个是最容易找到的。关闭后(遇到 ')' ),它首先被反转然后从历史记录中弹出,后面的字母被推入它的前一个元素。

就是这样,我希望它不会变得太乏味,做这个对我来说真的很有趣,因为我改进了代码并在此过程中学到了更多东西。保持健康,下次再见。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明

本文链接:https://www.qanswer.top/1590/55173005

posted @ 2022-08-30 05:56  哈哈哈来了啊啊啊  阅读(115)  评论(0编辑  收藏  举报