在JavaScript中将新值分配给传入函数的对象

9

我是JavaScript的新手(但有C++的经验),今天我写了这样的代码:

function foo(bar) {
    bar = "something else";
}
var x = "blah";
foo(x);
alert(x); // Alerts with "blah", but I was expecting it to alert with "something else"

我曾经被这个问题搞得很困惑,因为我看过Douglas Crockford的一些JavaScript视频,他说过 "JavaScript总是按引用传递"。

我可以解释这种情况是,JavaScript传递对象的引用,但这些引用是被复制的。这意味着在foo函数中,我正在为bar赋值一个新的引用,然后离开作用域,导致对x的引用未被改变。实际上,我们开始时有:

x   ---->"blah"

当调用 foo 时,bar 引用了相同的数据:

x   ---->"blah"
bar -----^

当"something else"被分配给bar时,会发生以下情况:

x   ---->"blah"
bar ---->"something else"

这是JavaScript中正在发生的准确模型吗?我有没有漏掉其他什么东西?

额外的问题是,有没有办法说“更改此变量引用的数据”?这种情况经常出现吗?还是可以轻松避免?

编辑:

在我观看的视频中,Douglas Crockford说“对象总是通过引用传递,而不是通过值传递”,这是正确的,但是传递给函数的参数是按值传递的,只是引用是按值传递的。


1
请查看https://developer.mozilla.org/zh-CN/docs/Talk:JavaScript/Guide/Obsolete_Pages/Defining_Functions。 - haim770
1
也许你看错了视频。JavaScript始终是按值传递的,即使这些值是对象的引用。 - Bergi
@Bergi - 我已经添加了一个链接到我记得的视频点。我误解了它,请看我的编辑。 - oggmonster
6个回答

7

您的解释非常准确。

首先,您有一个名为x的变量,它是一个指向字符串对象的引用。假设该内存地址为0x100,则x指向0x100,该地址中包含blah这些字节:

var x = "blah"; // x is 0x100 which references a string in memory

接下来,你将0x100传递给函数foo:
function foo(bar) {
    bar = "something else";
}

由于 JavaScript 中所有值都是按值传递的,即使是引用类型,JavaScript 也会在内存中复制该引用,这个复制后的引用现在在该函数中被称为 bar

foo(x); // Copies the value of x (a reference) to bar

此时,我们有两个不同的变量。 xbar。它们恰好都有相同的值 0x100。因此,如果您更改任一引用的对象的属性,则会影响到 xbar
但是,您正在将 bar 赋值为指向其他内容:
bar = "something else"; // Now references some other string we just created

现在,bar被重新分配为引用我们刚刚为其分配内存的新字符串。 bar不再具有0x100的值,而是具有一些其他地址的值(比如0x500)。当然,x仍然具有0x100的值,因为bar只是x的一个副本,而不是对x的引用。
因此,当您:
alert(x);

您仍将获得原始值,因为这就是x指向的内容。

第二个问题:

是否有任何方法可以更改此变量引用的数据?这种情况经常发生吗,还是可以轻松避免?

是的,只需将其包装在另一个对象中。例如:

var x = {Value: "blah"};
foo(x);

现在,我们有一个对象的引用,其中包含一个称为Value的属性,该属性包含对内存中某个字符串的引用。

foo中,我们可以执行以下操作:

bar.Value = "something else";

这会影响到 xValue 属性,因为 barx 都引用了同一个对象,而你从未更改过它们中的任何一个的值。
换句话说,你不能重新分配传递给函数的引用,因为你只是重新分配了一份副本。但是,你可以更改被引用的对象的属性,因为其他引用的副本都指向你正在更改的数据。

4

您的理解是正确的。

您可以更改对象中键的值,这使您可以执行类似于传递引用的操作:

function foo(bar) {
  bar.msg = "something else";
}
var x = { msg: "blah" };
foo(x);
alert(x.msg);

3
更准确地说,传递的是引用,实际上是按值传递(JavaScript 中的所有内容都是如此)。 - Mike Christensen

0

我知道你已经通过编辑回答了自己的问题...

我在观看的视频中,Douglas Crockford说“对象总是按引用传递,而不是按值传递”,这是正确的,但是函数的参数是按值传递的,只是引用是按值传递的。

但是因为那个编辑让我突然理解了这个过程...我承认我在这方面有些困惑,这里有一个代码示例和笔记,我认为这真正地证明了它。

x = {};
y = x; //point y reference to same object x is pointed to

console.log(x === y) //true both pointed at same object

function funky(o) //pass by value a reference to object
{
  o = null; //change reference to point to null away from object
}

funky(x);

console.log(x)//not null still an object

var myObj = {
  value: "Hello"
};

function change(localObj) {//pass reference to object
  localObj.value = "Bye";//change property through the reference
  localObj = null;//point the reference to null away from object
}

console.log(x === y) //still same reference;
change(myObj);
console.log(myObj.value) // Prompts "Bye"
console.log(myObj) //not null - its an object;
x = myObj; //point x to different object
console.log(x === y) //false pointed at different object;
console.log(x);
console.log(y);

https://codepen.io/waynetheisinger/pres/YewMeN


0
我看过的视频中,Douglas Crockford说“对象总是按引用传递,而不是按值传递”,这是正确的,但是函数的参数是按值传递的,只是引用是按值传递的。
这是不正确的。这正是所谓的按值传递。像Java一样,JavaScript只有按值传递。没有按引用传递。
你的理解是正确的。JavaScript中的每个值都是原始值或引用(指向对象的指针)。一个对象本身永远不能直接成为一个值。当你传递或分配一个引用(指向对象的指针)时,新的指针副本看到的是与原始指针相同的对象。但它们仍然是两个不同的指针变量。

-2
“JavaScript总是按引用传递”这句话其实是一个谎言和术语混淆。虽然有些地方存在一些灰色地带,但我默认按照这些评估策略的定义进行。
以下是我的论点和推理。如果你持有不同意见,请确保能够至少支持自己的观点。

按引用传递

按引用传递(对于许多人)意味着将值分配给参数后,会影响调用者中的绑定。

在按引用传递的求值策略(有时也称为传址调用)中,函数接收到一个隐式引用作为参数所使用的变量,而不是它的值的副本。 这通常意味着该函数可以修改(即赋值给)作为参数使用的变量 - 这是其调用者将看到的。

在 JavaScript 中,这并非如原帖所述是这种情况。重新赋值一个参数(可以视为具有动态提供值的本地变量)不会影响任何提供的参数。

有时,“按引用调用” [让人感到困惑] 被用来表示“按共享调用”或“按参考值调用”,如下所讨论的;真正的按引用调用只存在于像 C++ 和 VB 这样的语言中,但不适用于 JavaScript。

按[对象]共享调用

JavaScript 的调用约定完全可以用 按[对象]共享调用 语义来讨论。

所有 JavaScript 对象 都是 值;而所有基元值(它们是所有值的子集)都是不可变的。

调用共享的语义与按引用调用不同,因为函数内部对函数参数的赋值对调用者不可见。例如,如果传递了一个变量,则无法在调用者的范围内模拟对该变量的赋值。然而,由于函数可以访问与调用者相同的对象(不会进行复制),因此如果这些对象是可变的,则函数内部对这些对象的更改对调用者是可见的,这可能与按值调用的语义不同。 ultrayoshi的答案提供了这些共享的变化的示例,并且可以简单地解释:当表达式(例如变量访问)计算为对象时,并且该对象被传递给函数时,不会进行复制/克隆。

按(引用)值调用

虽然术语“按值调用[引用]”经常用来描述行为,但需要注意的是JavaScript 没有“引用”(或“非引用”值)Java / C#的意义不同,所以此术语存在微妙误导之处-至少没有说“按引用调用”,它有各种内涵,许多人都能理解低层次的说明。

在按值调用中,参数表达式被评估,并将结果值绑定到函数中相应的变量上。如果函数或过程能够为其参数分配值,则仅分配其本地副本-也就是说,在函数调用中传递的任何变量在函数返回时在调用方的作用域中都没有更改。

因为只有一个对象的“引用”被传递(而不是该对象的副本/克隆),因此语义仅为按共享调用。但是,在JavaScript中我避免使用此术语,因为这样会带来不必要的实现细节,并且还介绍了如何传递对象与原始值之间的区别。

“按值传递,其中值是一个引用”这个描述很常见(但不应被理解为按引用传递);另一个术语是共享调用。
因此,当我谈论JavaScript中的调用约定时,
我更喜欢使用共享调用来讨论行为,而避免使用按值/引用调用,因为它们有太多不同的“含义”,并拖入不必要的实现细节。

你能澄清一下你所说的“JavaScript没有‘引用’”是什么意思吗?我很确定var x = "Hello";会在运行时分配堆内存并创建一个字符串的引用,而x本身将保存一个4字节值(或64位CPU上的8字节)。也许你的意思是ECMA规范实际上没有在任何地方指定这个实现,只有它必须遵循的行为。在这种情况下,我认为可以说几乎所有的JavaScript引擎都会使用引用来实现这个行为,即使规范不要求这样做。 - Mike Christensen
1
JavaScript在Java/C#的意义上没有“引用(reference)”这个概念,但是它确实有与Java/C#相同的引用。 - newacct
1
“JavaScript的调用约定完全可以在[对象]共享语义中讨论。”实际上,这只是另一个名称的按值传递 - newacct
1
@user2864740: "C语言实现可能使用指针,Java实现则使用引用" 一个“引用”就是指向对象的指针。这就是引用的含义。所以你们说的是同一件事情。 - newacct
1
@user2864740:“引用”(http://en.wikipedia.org/wiki/Reference_%28computer_science%29)有一个基本的含义,语义上对应于许多语言所称的“指针”。一些语言可能使用“引用”来表示其他意思是另外一个问题。你基本上是在说,在这里谈论“引用”没有意义,因为它在不同的语言中没有一致的使用。好吧,无论你如何称呼它,在JavaScript中值的*语义*与Java完全相同,其中非原始类型被称为“引用”(在JLS中定义为指向对象的指针)。 - newacct
显示剩余4条评论

-3

像数字、字符串等原始值并不是按引用传递的,只有对象才是。例如:

var myObj = {
  value: "Hello"
};

function change(localObj) {
  localObj.value = "Bye";
}

change(myObj);
console.log(myObj.value) // Prompts "Bye"

2
这是完全不正确的。对象的引用也是按值传递的。会创建该引用的副本。 - Mike Christensen
嗯,@MikeChristensen,这个例子是正确的,但使用“引用(reference)”这个词可能不太合适。我认为这是一个基础问题,不需要更深入的理论解释:D - beagleknight

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