JavaScript按引用传递和按值传递的区别

422

我正在寻找一些全面的读物,关于JavaScript何时按值传递参数,何时按引用传递参数,并且何时在函数内修改传递参数的值会影响到函数外部的值,何时不会。我也对赋值给另一个变量是按引用还是按值进行,并且是否有任何不同于函数参数传递规则的情况感兴趣。

我已经做了很多搜索,发现了很多具体的例子(其中许多在SO上),可以从中开始拼凑出真正的规则,但我还没有找到一个单独、写得好的文档来描述它们。

此外,这种语言中是否有控制传递参数方式的方法?

以下是我想要理解的类型的问题。这些只是例子 - 我实际上是想了解该语言遵循的规则,而不仅仅是特定示例的答案。但是,以下是一些例子:

function f(a,b,c) {
   a = 3;
   b.push("foo");
   c.first = false;
}

var x = 4;
var y = ["eeny", "miny", "mo"];
var z = {first: true};
f(x,y,z);

在所有不同的类型中,x、y和z的内容在f函数范围之外何时发生更改?

function f() {
    var a = ["1", "2", "3"];
    var b = a[1];
    a[1] = "4";
    // what is the value of b now for all possible data types that the array in "a" might hold?
}

function f() {
    var a = [{yellow: "blue"}, {red: "cyan"}, {green: "magenta"}];
    var b = a[1];
    a[1].red = "tan";
    // what is the value of b now and why?
    b.red = "black";
    // did the value of a[1].red change when I assigned to b.red?
}
如果我想创建一个完全独立的对象副本(没有任何引用),那么最佳实践的方式是什么?
4个回答

719
我理解这其实很简单:
- JavaScript始终按值传递,但当变量引用对象(包括数组)时,“值”是对象的引用。 - 更改变量的值永远不会更改底层的原始类型或对象,它只会将变量指向新的原始类型或对象。 - 但是,更改变量引用的对象的属性确实会更改底层的对象。
因此,让我们看一些你的示例:
function f(a,b,c) {
    // Argument a is re-assigned to a new value.
    // The object or primitive referenced by the original a is unchanged.
    a = 3;
    // Calling b.push changes its properties - it adds
    // a new property b[b.length] with the value "foo".
    // So the object referenced by b has been changed.
    b.push("foo");
    // The "first" property of argument c has been changed.
    // So the object referenced by c has been changed (unless c is a primitive)
    c.first = false;
}

var x = 4;
var y = ["eeny", "miny", "mo"];
var z = {first: true};
f(x,y,z);
console.log(x, y, z.first); // 4, ["eeny", "miny", "mo", "foo"], false

例子2:

var a = ["1", "2", {foo:"bar"}];
var b = a[1]; // b is now "2";
var c = a[2]; // c now references {foo:"bar"}
a[1] = "4";   // a is now ["1", "4", {foo:"bar"}]; b still has the value
              // it had at the time of assignment
a[2] = "5";   // a is now ["1", "4", "5"]; c still has the value
              // it had at the time of assignment, i.e. a reference to
              // the object {foo:"bar"}
console.log(b, c.foo); // "2" "bar"

73
虽然从技术上来说是正确的,但我更愿意称 JavaScript 为“对象共享传递”(Pass By Object Sharing)参考链接。这可以避免混淆并转向“高层次”的视角。 - user166390
12
你所指的“混淆”是什么?对我来说,“按值传递”非常清晰明了。 - MEMark
34
改变一个变量的值不会改变底层的基础数据类型或对象。然而,改变由变量引用的对象的属性会改变底层的对象。这两句话结合在一起解决了很多疑惑。谢谢! - Amit Tomar
11
来自C语言,这太愚蠢和烦人了,声明声明声明... - Rafael
5
现在有一个数组 var users = [1,2,3,4]; 将其赋值给另一个变量 x_users = users; 当对 x_users 进行 push(5) 操作时,users 和 x_users 的值是相同的,因为它们都是按引用传递的。解决这个问题的方法之一是: 重新定义一个不同的数组来引用 users 的值,即 var x_users = users.slice(0); 然后对 x_users 进行 push(6) 操作,这样 users 和 x_users 就不会相互影响了。我花了一段时间才想到这个方法 :-) 希望这能帮助到别人。 - ralixyle
显示剩余7条评论

61

Javascript总是按值传递。然而,如果将一个对象传递给函数,则该“值”实际上是对该对象的引用,因此函数可以修改该对象的属性,但不能使函数外部的变量指向其他对象。

例如:

function changeParam(x, y, z) {
  x = 3;
  y = "new string";
  z["key2"] = "new";
  z["key3"] = "newer";

  z = {"new" : "object"};
}

var a = 1,
    b = "something",
    c = {"key1" : "whatever", "key2" : "original value"};

changeParam(a, b, c);

// at this point a is still 1
// b is still "something"
// c still points to the same object but its properties have been updated
// so it is now {"key1" : "whatever", "key2" : "new", "key3" : "newer"}
// c definitely doesn't point to the new object created as the last line
// of the function with z = ...

6
Array 是一个对象,因此它也会发生改变。 - shyammakwana.me
除了几乎所有的东西都是一个对象。 - Hritik
2
@Hritik - 除了所有不是对象的原始值。 - nnnnnn

29

2
虽然从技术上讲是正确的,但我更喜欢说JavaScript是“对象共享传递”。这避免了混淆并转向“高层次”视图。 - user166390
1
我创建了一个小工具来测试一下:http://jsfiddle.net/tkane2000/7weKS/1/ - tkane2000

19
  1. 像字符串、数字这样的基本类型变量始终作为按值传递。
  2. 数组和对象根据以下两个条件之一而被传递扩展引用或按值传递。

    • 如果您正在使用新对象或数组更改该对象或数组的值,则它是按值传递。

      object1 = {item: "car"}; array1=[1,2,3];

    在此处,您将新对象或数组分配给旧对象。 您没有更改旧对象的属性值,因此它是按值传递。

    • 如果您正在更改对象或数组的属性值,则它是通过引用传递。

      object1.item= "car"; array1[0]=9;

    在此处,您正在更改旧对象的属性值。 您没有将新对象或数组分配给旧对象,因此它是通过引用传递。

Code

    function passVar(object1, object2, number1) {

        object1.key1= "laptop";
        object2 = {
            key2: "computer"
        };
        number1 = number1 + 1;
    }

    var object1 = {
        key1: "car"
    };
    var object2 = {
        key2: "bike"
    };
    var number1 = 10;

    passVar(object1, object2, number1);
    console.log(object1.key1);
    console.log(object2.key2);
    console.log(number1);

Output: -
    laptop
    bike
    10

2
将上述代码放入控制台并查看...值会发生变化。 - Mukund Kumar
12
关于称呼 JavaScript 中的 "传递" 行为,存在一种历史悠久的术语争议。我倾向于回避这个争论,将 JavaScript 对象和数组的行为称之为 "传递指针"。对于数组和对象,它们总是通过指针进行传递。如果您修改了传入的内容(访问指针),则原始内容也会被修改。如果您将不同的数组或对象分配给指针变量,则原始内容不会被修改,因为您的变量现在"指向"不同的数组或对象。这其中大部分是一个“术语争议”,因为实际发生的事情是没有争议的。 - jfriend00
11
对于那些喜欢声称“一切都是按值传递”的人来说,即使在某个技术层面上你认为自己是正确的,这并没有帮助新手理解问题。任何好的解释都必须解释对象和原始数据类型如何传递,以便新手能够了解实际应用中的区别,因为在这样的问答中,目标是清晰明了的解释,可以供那些不了解更细节的实现细节或技术术语含义的人使用。 - jfriend00
4
事实上,“传递”并没有什么特别的。它的工作方式与赋值或任何其他操作相同。一旦您理解所有值都是基元值或指向对象的指针,并且不将“对象”本身作为值来讨论,那么就不会产生混淆。此外,在 JavaScript 中,传递和赋值的语义与 Java 中的语义相同。在 StackOverflow 上,Java 通常被描述为“一切都是按值传递”,并解释了所有非基元类型都是指向对象的指针。 - newacct
8
这里的重点是向那些不是经验丰富的开发人员解释,仅仅说所有内容都是“按值传递”的并没有足够的说明。这并没有解释原始类型和数组在传递或分配时有何不同之处。你可以认为它在技术上是正确的,但对于不太精通开发的人来说,这是一个不充分的解释。你在评论中提到的“一旦你理解了……”表明除非你理解其他内容,否则这不足以做出解释——因此,你不能仅仅告诉新手“所有内容都是按值传递”的就完事了。 - jfriend00
显示剩余2条评论

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