Javascript指针/引用的疯狂。有人能解释一下吗?

66

Javascript通过引用传递对象。这很有道理。但是一旦您开始操作这些对象,一切都会表现出看似不直观的方式。让我举个例子:

var a, b;

a = {}
b = a;
a['one'] = {};

console.log( JSON.stringify(a) );
// outputs: {"one":{}}

console.log( JSON.stringify(b) );
// outputs: {"one":{}}

这很好,因为现在b有了一个指向a的指针,所以预期将东西分配给a也会影响b。但是如果我这样做:
a = a['one'];

console.log( JSON.stringify(a) );
// outputs: {}

console.log( JSON.stringify(b) );
// outputs: {"one":{}}

这让我感到惊讶。我本以为ab仍然是相同的(并且应该是{},因为a['one']之前被设置为{},而a被设置为a['one'])。

但事实并非如此。当a被分配给新值时,它似乎失去了对b的引用,但b仍保持着a在失去对b的引用之前被设置的值。

但如果我这样做:

a['two'] = 2;

console.log( JSON.stringify(a) );
// outputs: {"two":2}

console.log( JSON.stringify(b) );
// outputs: {"one":{"two":2}}

什么?a已经明显失去了对b的引用,但是b似乎仍然对a有一些引用。

空对象{}是否指向内存中的某个位置,因此每个引用它的变量现在都指向同一个位置?

有没有对此有深刻理解的人可以给我解释一下?


请参见 https://dev59.com/5nRB5IYBdhLWcg3wxZ7Y。 - Crescent Fresh
4
“JavaScript通过引用传递对象”-并不是这样。它是按值传递的,但在对象的情况下,传递的值是指向对象的引用而不是对象本身的副本。这是一个重要的区别。如果你不是在谈论函数参数,那么谈论“传递”值或引用就没有什么意义了。但无论如何:如果它是按引用传递的,那么你可以通过更改第二个变量b指向的对象来改变原始变量a指向的对象,但你绝对不能这样做(无论是否在函数中)。 - nnnnnn
@nnnnnn 我现在很困惑。如果你假设Javascript通过引用完成所有操作,那么通过改变b来改变a指向的对象的指针是如何实现的呢?看起来ab是相互独立的引用,就像指针一样,而且它似乎仅通过引用传递,只是不传递变量的引用,而是传递变量所指向的东西的引用。 - Seth Carnegie
抱歉,不,我说它不是按引用传递的,然后说“如果”它按引用传递(但实际上不是),你可以通过改变 b 来改变 a 指向的位置 - “这是绝对不可能的”。JavaScript 绝对不会按引用传递。所以你说 ab 就像独立的指针,可能指向相同的对象,也可能不是。(但除了讨论函数参数之外,使用“传递”这个词没有意义。) - nnnnnn
1
@ Seth Carnegie:"按引用传递" 是标准的非语言特定编程术语,许多语言都支持,而不会向程序员公开指针,但JavaScript却不支持。你所说的是正确的:"通过值传递引用"就是JavaScript对象发生的情况。很抱歉,之前我没有说清楚。由于术语问题,我认为我们一直在打圆场,对此感到抱歉。 - nnnnnn
显示剩余4条评论
5个回答

209

按照你的示例逐行进行:

a = {}

a现在引用了新对象。

b = a;

b现在引用了与a相同的对象。要注意的是,它并没有引用a

a['one'] = {};
新对象现在有一个索引 'one',它引用另一个新对象。
当你执行
a = a['one'];
你正在将 a 设置为引用 a['one'],而这是你执行 a['one'] = {} 时创建的新对象。而 b 仍然引用使用 a = {} 创建的对象。
当你说 "a 失去了对 b 的引用" 时,你混淆了问题,因为 a 不引用 b,反之亦然。 ab 引用 对象,它们可以被引用到其他对象。像这样:
使用 a = {}; b = a,你会得到
a
 \
  \
   { }
  /
 /
b

然后使用a['one'] = {}得到

a
 \
  \
   { one: { } }
  /
 /
b

然后使用a = a['one'],你就能得到:

a - - - - 
          \
   { one: { } }
  /
 /
b

4
有时候,a = {} 这样简单的赋值语句会失去指针的明确性。如果有帮助的话,在你的脑海中将 {} 替换为 "new object()",并记住它正在创建一个新的引用。 - Will Shaver
6
啊呀,失败了。我发表了一个(我认为)很好的解释方法,然后看到了这个可视化... 叹气 ... +1。 - riwalk

47

:P 你正在深入纠缠不清的细节,我很高兴你问了这个问题,因为最终你会更加明智。

不要从指针的角度来看待它,因为我认为那是让你感到困惑的地方。相反,以堆(或者说“内存”)和符号表的角度来考虑它。

让我们从你代码的前几行开始:

var a, b;

a = {}
b = a;

你所做的是在堆上创建了一个对象和两个符号表中的符号。其大致如下:


符号表

+--------+-----------------+
| Symbol | Memory Location |
+--------+-----------------+
|      a |        0x400000 |
+--------+-----------------+
|      b |        0x400000 |
+--------+-----------------+

堆(Heap):

+----------+-----------------+
| Location | Value           |
+----------+-----------------+
| 0x400000 | <object val 1>  |
+----------+-----------------+

事情变得有趣的地方在于:对象拥有自己的“符号表”(通常只是哈希表,但称其为符号表可以使其更清晰)。

现在,在您的下一条语句之后,您需要考虑三件事:全局符号表、<object val 1> 的符号表和堆。

运行以下代码:

a['one'] = {}

现在的情况如下:


全局符号表

+--------+-----------------+
| Symbol | Memory Location |
+--------+-----------------+
|      a |        0x400000 |
+--------+-----------------+
|      b |        0x400000 |
+--------+-----------------+

<object val 1>的符号表

+--------+-----------------+
| Symbol | Memory Location |
+--------+-----------------+
|    one |        0x400004 |
+--------+-----------------+

堆(heap):

+----------+-----------------+
| Location | Value           |
+----------+-----------------+
| 0x400000 | <object val 1>  |
+----------+-----------------+
| 0x400004 | <object val 2>  |     <---we created a new object on the heap
+----------+-----------------+

.


现在你运行了以下代码:

a = a['one'];

这应该是一个微不足道的更改。结果如下:


全局符号表

+--------+-----------------+
| Symbol | Memory Location |
+--------+-----------------+
|      a |        0x400004 |
+--------+-----------------+
|      b |        0x400000 |
+--------+-----------------+

<object val 1>的符号表

+--------+-----------------+
| Symbol | Memory Location |
+--------+-----------------+
|    one |        0x400004 |
+--------+-----------------+

堆(Heap):

+----------+-----------------+
| Location | Value           |
+----------+-----------------+
| 0x400000 | <object val 1>  |
+----------+-----------------+
| 0x400004 | <object val 2>  | 
+----------+-----------------+

沿着内存地址向堆区查找,有助于理解为什么会得到你所看到的输出。

现在情况变得更加有趣,因为你正在执行以下操作:

a['two'] = 2;

好的,让我们一步步来。

  • a 指向内存位置 0x400004,该位置包含 <object val 2>
  • <object val 2> 是一个空对象,因此它的符号表一开始是空的
  • 通过执行这行代码,我们将变量 'two' 添加到 <object val 2> 的符号表中。

如果你还没有看够这些图表,那你很快就会疲倦。现在的情况如下:


全局符号表

+--------+-----------------+
| Symbol | Memory Location |
+--------+-----------------+
|      a |        0x400004 |
+--------+-----------------+
|      b |        0x400000 |
+--------+-----------------+

<object val 1>的符号表

+--------+-----------------+
| Symbol | Memory Location |
+--------+-----------------+
|    one |        0x400004 |
+--------+-----------------+

<object val 2>的符号表

+--------+-----------------+
| Symbol | Memory Location |
+--------+-----------------+
|    two |        0x400008 |
+--------+-----------------+

堆(Heap):

+----------+-----------------+
| Location | Value           |
+----------+-----------------+
| 0x400000 | <object val 1>  |
+----------+-----------------+
| 0x400004 | <object val 2>  | 
+----------+-----------------+
| 0x400008 | 2 (literal val) |    <-- yes, even integers are stored on the heap
+----------+-----------------+        in JavaScript.

如果你勤奋地花时间跟踪内存位置,你会发现你的浏览器显示了正确的输出。


3
哈,你的图比我的详细得多(我只会画箭头图)。+1 - Seth Carnegie
4
@SethCarnegie,是的,但“you's”用更少的空间传达了相同的意思。 - riwalk
有没有一种工具可以显示JavaScript变量的内存位置?或者,有没有其他主动的方法来调试内存? - P.Brian.Mackey
1
@P.Brian.Mackey...据我所知没有。这是一个很好的思路,但现代JavaScript引擎进行了极致优化,因此你不太可能能够使用工具给出完全像这样的图片。(例如:符号表被废弃,而采用偏移量,对象的实际结构可以更复杂,使用隐藏类结构等等...这是一个漫长的兔子洞...现在坚持心理模型 :P) - riwalk

9

把匿名对象看作自己有一个名称:

a = {}; // The variable "a" now points to (holds) an anonymous object.
b = a; // "b" points to the same anonymous object held by "a".
a = 123; // "a" now holds some other value.
b; // "b" still holds the anonymous object.

关键在于要记住变量保存的是对于对象的引用,而不是其他变量的引用。同一个对象可能被任意数量的变量引用。


6

Javascript中的对象可以存在于没有名称的情况下。例如:

{}

是一个字典对象的新实例。

a = {};

创建一个新的字典对象并使“a”引用它。现在,a是指向该字典对象的引用。
b = a;

使b指向相同的基础对象。然后您可以将a指向其他位置:

a = "hi";

b仍然指向之前的同一个字典对象。b的行为与您改变a指向的内容无关。


0
据我所知,您覆盖了变量a,因此我猜测引擎将其保存在另一个内存空间中,而变量b仍然指向旧的变量a的内存地址(某种方式没有被销毁)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接