JavaScript中的作用域表现奇怪

18

Javascript中传递的是对象的引用。这意味着该对象从任何地方发生的更改都应该被反映出来。 在这种情况下,console.log(a)的期望输出结果是 {}。

function change(a,b) {
    a.x = 'added';
    a = b;//assigning a as {} to b
}
a={}
b={}
change(a,b);
console.log(a); //expected {} but output {x:'added'}
console.log(b)

这里发生了什么?据我所知,这不应该是由于函数作用域而引起的。 谢谢


1
对象引用作为函数参数传递。因此,无论您在函数内更改什么,都会影响实际对象。 - Vigneswaran Marimuthu
当您在函数或其他任何地方更改对象的属性时,它是通过引用工作的,但如果您将另一个值分配给它,则全局变量不会像值变为本地变量一样发生更改。 - iamawebgeek
2
在函数内部将 b 赋值给 a 没有任何效果,因为函数作用域中的 a 是一个不同的变量,而不是全局作用域中的 a - GOTO 0
9
我喜欢你说你知道对象是通过引用传递的,然后表达对对象通过引用传递感到非常震惊和惊讶的方式。 - Lightness Races in Orbit
1
这只是令人困惑的原因是函数参数中的变量使用与全局变量相同的名称。如果函数参数名称为foobar,那么可能更容易理解它的工作方式。 - Brandon
显示剩余8条评论
6个回答

37

如果您添加了另一行,您就可以更清楚地了解正在发生的事情:

function change(a,b) {
    a.x = 'added';
    a = b;
    a.x = 'added as well';
};
a={};
b={};
change(a,b);
console.log(a);  //{x:'added'}
console.log(b);  //{x:'added as well'}

当您执行a = b时,您正在将本地变量a分配给b所持有的引用。


2
谢谢,尼古拉的答案实际上解决了我的问题。 - Suman Lama
这真是一个令人费解的问题。用几行代码进行了很好的解释! - Nathan Tregillus

21

您说得没错,对象是按引用传递的,函数中对对象所做的任何更改都会在所有地方反映出来。这正是为什么在函数中添加x属性会修改它外部的对象。

您忽略了一点,即a = b;这行代码并未修改对象,而是修改了对象的引用。如果需要设置引用,可以将这两个对象放在另一个容器对象/数组中一起传入:

function change(container) {
    container.a.x = 'added';
    container.a = container.b;//assigning a as {} to b
}
var container = { a: {}, b: {}};
change(container);
console.log(container.a);
console.log(container.b)

5
但是对象并没有通过引用传递! a = b 只会修改本地引用(变量)! - Bergi
@zazu:可能我只是在挑剔“按引用传递”这个术语。 - Bergi
5
“你是正确的,对象的传递是按引用方式进行的” - 不,它们不是。JavaScript 严格按值传递,并且 OP 的代码很好地证明了这一事实。如果 JavaScript 是按引用传递的,那么 OP 的代码将可以工作,而他也不会首先提出这个问题。 - Jörg W Mittag
1
我从Java世界熟悉的术语,在这里同样适用,称为“按引用传递的副本”。 - Brandon
2
@Bergi:那个链接就是答案 :) 它准确地解释了OP的问题。 - Lightness Races in Orbit
显示剩余5条评论

13
在您的函数上下文中,变量'a'与函数外部的变量'a'不同。此代码在语义上等同于您的代码:
function change(foo,bar) {
    foo.x = 'added';
    foo = bar;//assigning foo as {} to bar
}
a={}
b={}
change(a,b);
console.log(a); //expected {} but output {x:'added'}
console.log(b)

在这种情况下,很明显'foo'变量只存在于函数内部,而执行foo = bar不会改变a,因为引用是按值传递的。


2
这个人懂的。function change(a,b) 创建了本地变量ab,所以赋值a = b是针对本地变量a的赋值,而不是全局变量。 - RobG
换句话说:foo 持有一个指向与 a 相同的对象的引用。foo 不是对 a 的引用。 - Bergi

9
在JavaScript中,对象是通过它们的引用来传递的。不,它们并不是。ECMAScript/JavaScript严格采用按值传递的方式(更准确地说,它采用了一种特殊情况的按值传递方式,也就是共享调用,这是按值传递的一种特例)。这只是正常的按值传递。你的困惑源于你错误地认为ECMAScript/JavaScript是按引用传递的,事实上它并不是。ECMAScript使用按值传递,更准确地说是一种特殊情况的按值传递,其中传递的值始终是一个指针。这种特殊情况有时也被称为共享调用、调用对象共享或调用对象。这与Java(用于对象)、C#(默认情况下用于引用类型)、Smalltalk、Python、Ruby以及几乎所有创建过的面向对象语言都使用相同的约定。注意:某些类型(例如数字)实际上是直接按值传递的,而不是通过一个中间指针传递的。然而,由于这些类型是不可变的,在这种情况下,按值传递和调用对象共享之间没有明显的行为差异,因此您可以通过简单地将所有内容视为调用对象共享来大大简化您的精神模型。只需将这些特殊情况解释为您无需担心的内部编译器优化即可。下面是一个简单的示例,您可以运行它来确定ECMAScript(或任何其他语言,在您翻译后)的参数传递约定:

function isEcmascriptPassByValue(foo) {
  foo.push('More precisely, it is call-by-object-sharing!');
  foo = 'No, ECMAScript is pass-by-reference.';
  return;
}

var bar = ['Yes, of course, ECMAScript *is* pass-by-value!'];

isEcmascriptPassByValue(bar);

console.log(bar);
// Yes, of course, ECMAScript *is* pass-by-value!,
// More precisely, it is call-by-object-sharing!

如果你熟悉C#,就可以很好地理解值类型和引用类型的传值和传引用之间的区别。因为C#支持所有四种组合:值类型的传值(“传统的传值”)、引用类型的传值(共享调用、对象调用、对象共享调用,如ECMAScript中所示)、引用类型的传引用,以及值类型的传引用。
(实际上,即使你不了解C#,这也不太难理解。)
// In C#, struct defines a value type, class defines a reference type
struct MutableCell
{
    public string value;
}

class Program
{
    // the ref keyword means pass-by-reference, otherwise it's pass-by-value
    // You must explicitly request pass-by-reference both at the definition and the call
    static void IsCSharpPassByValue(string[] foo, MutableCell bar, ref string baz, ref MutableCell qux)
    {
        foo[0] = "More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value.";
        foo = new string[] { "C# is not pass-by-reference." };

        bar.value = "For value types, it is *not* call-by-sharing.";
        bar = new MutableCell { value = "And also not pass-by-reference." };

        baz = "It also supports pass-by-reference if explicitly requested.";

        qux = new MutableCell { value = "Pass-by-reference is supported for value types as well." };
    }

    static void Main(string[] args)
    {
        var quux = new string[] { "Yes, of course, C# *is* pass-by-value!" };

        var corge = new MutableCell { value = "For value types it is pure pass-by-value." };

        var grault = "This string will vanish because of pass-by-reference.";

        var garply = new MutableCell { value = "This string will vanish because of pass-by-reference." };

        // the first two are passed by value, the other two by reference
        IsCSharpPassByValue(quux, corge, ref grault, ref garply);

        Console.WriteLine(quux[0]);
        // More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value.

        Console.WriteLine(corge.value);
        // For value types it is pure pass-by-value.

        Console.WriteLine(grault);
        // It also supports pass-by-reference if explicitly requested.

        Console.WriteLine(garply.value);
        // Pass-by-reference is supported for value types as well.
    }
}

7

好的,所以您已经发现JavaScript对象具有引用语义,因此修改引用会对原始作用域中的同一对象产生影响。

您还需要意识到的是,= 不属于这些规则的一部分;它不仅执行赋值操作,而且还将引用重新绑定到新对象。

从底层来看,这基本上就是您最初形成引用的方式。


1
对象具有引用语义(即始终传递指针,而不是对象本身),参数传递始终是按值传递,而从不按引用传递。(后者是 OP 漏掉的关键点,他似乎认为 JavaScript 是按引用传递的,这是错误的。) - Jörg W Mittag
@JörgWMittag:可以争论JavaScript是按引用传递的,也可以争论它不是。术语在社区中使用不一致,不能断言一个是“不正确”的,而另一个是“正确”的。你可以说的是JavaScript采用共享传递,这是唯一可以应用于此处的精确、准确和客观的术语。其余的只是个人偏好,在不精确的情况下选择什么术语。 - Lightness Races in Orbit
传递共享是传递值的特殊情况。OP认为JavaScript是按引用传递的,他在假设JavaScript是按引用传递的情况下编写了代码,如果JavaScript是按引用传递的,代码可以工作,但实际上不是按引用传递的,所以无法工作。我知道您不想相信互联网上的陌生人,但您相信JavaScript吗?那么您可以自己问它:... - Jörg W Mittag
function isEcmascriptPassByValue(foo) { foo = 'No, ECMAScript is pass-by-reference.'; return; }; var bar = 'Yes, of course, ECMAScript *is* pass-by-value!'; isEcmascriptPassByValue(bar); alert(bar); // Yes, of course, ECMAScript *is* pass-by-value! - Jörg W Mittag
2
@JörgWMittag:你好像没有读懂我说的任何话。你正在应用自己狭隘的个人定义来做出一个具体的事实陈述,但你不能这样做:两者的普遍接受的含义都不足以涵盖JavaScript的语义。请阅读我给你链接的文章。 - Lightness Races in Orbit

-1

这应该能帮助您解决问题:

var obj = {}, anotherObj = {};
// in case if they are not global, make them global or define parent scope to be able to modify inside the function
window.obj = obj;
window.anotherObj = anotherObj;
function swap(a, b) {
  window[a].newProp = 'XYZ';
  window[a] = window[b]; // now obj is gone, because replaced with anotherObj
}
swap('obj','anotherObj');
console.log(obj); // now it would give Object {}

我不明白你的回答与问题有什么关系。而且你的代码只是表明即使对象和数组也不是通过引用传递的(如果是这样,你就不需要那个“作用域”)。 - Bergi
@Bergi的想法与Nikola的答案是一样的。唯一的区别是容器是window对象,变量名作为字符串传递给函数。如果OP想要解决他的问题,而不仅仅是对发生了什么进行解释,这个解决方案正是他所需要的。 - iamawebgeek
3
默认情况下,对象和数组是按引用传递的。- 不是这样的。JavaScript 严格按值传递,而且原帖的代码很好地证明了这一点。如果 JavaScript 是按引用传递的,原帖的代码就可以工作了,作者也不会首先提出这个问题。 - Jörg W Mittag
@JörgWMittag,争论如何理解这个短语没有意义。我已经与Bergi进行了辩论并表达了我的观点。关键是解决方案是否符合OP的要求。 - iamawebgeek

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