JavaScript 传递引用还是传值?

475

JavaScript是按引用传递还是按值传递的?

以下是来自JavaScript: The Good Parts的一个例子。我对rectangle函数中的my参数感到非常困惑。它实际上是undefined,并在函数内部被重新定义。没有原始引用。如果我从函数参数中删除它,则无法访问内部区域函数。

这是闭包吗?但没有返回函数。

var shape = function (config) {
    var that = {};
    that.name = config.name || "";
    that.area = function () {
        return 0;
    };
    return that;
};

var rectangle = function (config, my) {
    my = my || {};
    my.l = config.length || 1;
    my.w = config.width || 1;
    var that = shape(config);
    that.area = function () {
        return my.l * my.w;
    };
    return that;
};

myShape = shape({
    name: "Unhnown"
});

myRec = rectangle({
    name: "Rectangle",
    length: 4,
    width: 6
});

console.log(myShape.name + " area is " + myShape.area() + " " + myRec.name + " area is " + myRec.area());
13个回答

873

基本类型按值传递,而对象按“引用的副本”传递。

具体地说,当您传递一个对象(或数组)时,您(看不见地)传递了对该对象的引用,可以修改该对象的内容,但如果尝试覆盖引用,则不会影响调用者持有的引用副本 - 即引用本身按值传递:

function replace(ref) {
    ref = {};           // this code does _not_ affect the object passed
}

function update(ref) {
    ref.key = 'newvalue';  // this code _does_ affect the _contents_ of the object
}

var a = { key: 'value' };
replace(a);  // a still has its original value - it's unmodfied
update(a);   // the _contents_ of 'a' are changed

43
虽然不太流行,但是针对对象的行为实际上被称为“共享调用(call by sharing)”,详见:http://en.wikipedia.org/wiki/Call_by_sharing#Call_by_sharing - Ioan Alexandru Cucu
8
个人认为,“copy of reference”更直观。;-) - Alnitak
9
我回滚了你的编辑,因为它完全改变了它的语义。对于得票率如此之高的答案进行如此重大的更改也是完全不合适的! - Alnitak
1
ref = {} 和 ref = new Object() 是一样的,因此我不认为它是替换,而是创建了一个同名的局部变量,局部名称优先于全局名称。在那个 replace 函数中,你可以使用 window.ref 来访问全局的 ref,它实际上从未被替换,因为即使你离开该函数,它仍保留着它的原始值。虽然这个答案可能是正确的,但我认为解释一下为什么它起作用会很有帮助。 - user3015682
16
如果你要使用“参考复制”的短语,那么你也可以称原始类型为“值的复制”。这就是为什么两者实际上都只是“按值传递”。无论该值是数字、布尔值、字符串还是引用,都会传递该值的副本。 - gman
显示剩余14条评论

73

就像这样考虑:

每当您在ECMAscript中创建一个对象时,该对象会在神秘的 ECMAscript通用位置 中形成,任何人都无法获取。 您只会收到该对象在此神秘地方的引用。

var obj = { };

即使 obj 只是指向对象的引用(该对象位于那个特殊而美妙的地方),因此,您只能传递此 引用。有效地,访问 obj 的任何代码都将修改远在天边的 对象


47
在JavaScript中,与其他所有内容一样,引用本身也是按值传递的。 - Pointy
2
@Pointy 参考的值是什么?参考是一种类型吗?我认为这个文字游戏毫无意义。 - albanx
2
@albanx 我知道这很令人沮丧,但每个专业领域都有专业术语。 "引用" 的意思类似于 C 或 C++ 中的 "指针"(C++ 有指针和引用两种)。然而,在像 JavaScript 或 Java 这样的语言中,作为特定对象的 "值" 只能是对该对象的 引用。因此,它实际上不是一种类型,而是对值实际上是什么的描述。 - Pointy
2
@albanx 我会把这个意见传达给2012年的我 :) - Pointy
5
我被这次交流中的某个地方所震撼,有点令人沮丧地意识到,即使我已离开这个世界,人们仍会对我的回答和评论发表看法。 - Pointy
显示剩余3条评论

70

我的意见是,JavaScript参数是按值传递还是按引用传递并不重要。真正重要的是赋值和变异。

我在此链接中写了一篇更详细的解释。

当您传递任何内容(无论是对象还是原始数据类型),JavaScript所做的就是在函数内部分配一个新变量... 就像使用等号(=)一样。

在函数内部,参数的行为方式与仅使用等号(=)分配新变量的方式完全相同... 请看以下简单示例。

var myString = 'Test string 1';

// Assignment - A link to the same place as myString
var sameString = myString;

// If I change sameString, it will not modify myString,
// it just re-assigns it to a whole new string
sameString = 'New string';

console.log(myString); // Logs 'Test string 1';
console.log(sameString); // Logs 'New string';

如果我将myString作为参数传递给一个函数,它的行为就像我简单地将其赋值给一个新变量一样。现在,让我们用一个函数来做同样的事情,而不是进行简单的赋值操作。

function myFunc(sameString) {

  // Reassignment... Again, it will not modify myString
  sameString = 'New string';
}

var myString = 'Test string 1';

// This behaves the same as if we said sameString = myString
myFunc(myString);

console.log(myString); // Again, logs 'Test string 1';

你可以在传递对象到函数时修改它们的唯一原因是你没有对其进行重新分配...相反,对象可以被更改或突变...同样,它的工作方式也是这样。

当你将对象传递给函数时,你可以对它们所做的任何更改都会保留下来,这是因为你没有给它们分配新的值。相反,你只是修改了对象本身。这就是为什么它可以像平常一样工作的原因。

var myObject = { name: 'Joe'; }

// Assignment - We simply link to the same object
var sameObject = myObject;

// This time, we can mutate it. So a change to myObject affects sameObject and visa versa
myObject.name = 'Jack';
console.log(sameObject.name); // Logs 'Jack'

sameObject.name = 'Jill';
console.log(myObject.name); // Logs 'Jill'

// If we re-assign it, the link is lost
sameObject = { name: 'Howard' };
console.log(myObject.name); // Logs 'Jill'

如果我将myObject作为参数传递给一个函数,它会表现得就像我简单地将它赋值给了一个新变量。同样的行为也适用于带有函数的情况。

function myFunc(sameObject) {
  // We mutate the object, so the myObject gets the change too... just like before.
  sameObject.name = 'Jill';

  // But, if we re-assign it, the link is lost
  sameObject = {
    name: 'Howard'
  };
}

var myObject = {
  name: 'Joe'
};

// This behaves the same as if we said sameObject = myObject;
myFunc(myObject);
console.log(myObject.name); // Logs 'Jill'

每当您将变量传递给函数时,您都是在“分配”给参数名称,就像使用等号=一样。
永远记住等号=表示赋值。而将参数传递给函数也意味着赋值。它们是相同的,这两个变量以完全相同的方式连接。
只有在改变对象的基础对象时,修改变量才会影响不同的变量。
区分对象和原始类型没有意义,因为其工作方式与如果您没有函数,只使用等号来分配给新变量的方式完全相同。

2
这很简单,就是“按复制传递”和“按引用传递”,以传达所有相关含义。你是得到“它自己的东西”还是只关心“那个东西”。 - GL_Stephen
1
你的作业(去掉&符号),类比似乎只是对传值方式的解释,不是吗?那为什么不直接说呢?当你谈论传值方式时,为什么要说传值方式不相关呢? - barlop
3
很好的解释,Ray! - jayvatar
Ray Perea:我的两分钱... JavaScript 是通过引用还是值传递参数都无关紧要,真正重要的是赋值与突变。 GL_Stephen:传递以副本为基础和传递引用很简单,可以传达所有相关的含义。 嗯,你们意识到这里也存在性能问题,对于传递包含对象数组的大型数组,按值传递比按引用传递慢得多。 - Synetech

41

JavaScript中,函数参数的传递方式有按值传递和按共享传递,但从来没有按引用传递!

按值传递

原始类型是按值传递的:

var num = 123, str = "foo";

function f(num, str) {
  num += 1;
  str += "bar";
  console.log("inside of f:", num, str);
}

f(num, str);
console.log("outside of f:", num, str);

函数作用域内的重新赋值在外部作用域中不可见。

这也适用于String,它们是复合数据类型,但却是不可变的:

var str = "foo";

function f(str) {
  str[0] = "b"; // doesn't work, because strings are immutable
  console.log("inside of f:", str);
}

f(str);
console.log("outside of f:", str);

按共享调用

对象,也就是所有非基本类型,都是通过共享传递的。持有对象引用的变量实际上只持有该引用的副本。如果JavaScript采用按引用调用的评估策略,变量将持有原始引用。这是按共享和按引用之间的关键区别。

这种区别的实际后果是什么?

var o = {x: "foo"}, p = {y: 123};

function f(o, p) {
  o.x = "bar"; // Mutation
  p = {x: 456}; // Reassignment
  console.log("o inside of f:", o);
  console.log("p inside of f:", p);
}

f(o, p);

console.log("o outside of f:", o);
console.log("p outside of f:", p);

变异是指修改一个现有对象的某些属性。变量所绑定并引用该对象的参考副本仍然相同。因此,变异在调用者范围内是可见的。

重新赋值意味着替换与变量绑定的参考副本。由于它只是一个副本,持有相同参考的其他变量保持不受影响。因此,重新分配不像使用调用按引用评估策略那样在调用者范围内可见。

有关 ECMAScript 中 评估策略 的更多信息。


我看过的关于Javascript的最好和最重要的答案之一。 - Eric
它不仅适用于函数调用,也适用于简单赋值。 - Eric

22
与 C 语言一样,最终所有东西都是按值传递的。 不同于 C 语言,你不能实际上回溯并传递变量的位置,因为它没有指针,只有引用。

而且它所具有的引用都是对象,不是变量。 有几种方法可以实现相同的结果,但它们必须手动完成,不能只在调用或声明站点添加一个关键字。


3
这实际上是这里答案中最正确的一个。如果你深入研究V8或其他竞争引擎,就会发现函数调用是如何实现的。 - joekarl
在底层,我敢打赌对象就是指针。对象参数是一个新创建的指针,它指向与传入的指针相同的地址。 - user3015682

9

JavaScript是按值传递的。

对于原始数据类型,会传递原始数据的值。对于对象,会传递对象的引用“值”。

以下是一个使用对象的示例:

var f1 = function(inputObject){
    inputObject.a = 2;
}

var f2 = function(){
    var inputObject = {"a": 1};
    f1(inputObject);
    console.log(inputObject.a);
}

调用f2会导致将“a”的值更新为2而不是1,因为传递了引用并更新了引用中的“a”值。

使用基本数据类型的示例:

var f1 = function(a){
    a = 2;
}
var f2 = function(){
    var a = 1;
    f1(a);
    console.log(a);
}

调用f2会将“a”值打印为1。


5

为了创建一个使用常量的简单示例...

const myRef = { foo: 'bar' };
const myVal = true;

function passes(r, v) {
  r.foo = 'baz';
  v = false;
}

passes(myRef, myVal);

console.log(myRef, myVal); // Object {foo: "baz"} true

5

实际上,Alnitak是正确的,这使得理解起来很容易,但归根结底,在JavaScript中,所有东西都是按值传递。

那么对象的“值”是什么? 它是对象引用。

当您传入一个对象时,您会得到此值的副本(因此是Alnitak所描述的“引用副本”)。 如果更改此值,则不会更改原始对象; 您正在更改该引用的副本。


5
这并没有澄清问题,反而让人更加困惑。 - Guillermo Siliceo Trueba
一个对象的“值”是什么?它就是对象引用。简单而完美! - Can Mingir

3

没有纯粹性,我认为在JavaScript中模拟按引用传递的标量参数的最佳方法是使用对象,就像之前一个回答所说的那样。

但是,我的做法有些不同:

我将对象赋值放在函数调用内部,这样人们可以看到函数调用附近的引用参数。这提高了源代码的可读性。

在函数声明中,我像注释一样放置属性,出于同样的原因:可读性。

var r;

funcWithRefScalars(r = {amount:200, message:null} );
console.log(r.amount + " - " + r.message);


function funcWithRefScalars(o) {  // o(amount, message)
  o.amount  *= 1.2;
  o.message = "20% increase";
}

在上面的例子中,null 明确表示一个输出引用参数。
退出:
240 - 20% Increase

在客户端,应该用alert代替console.log
★ ★ ★
另一种方法可能更易读:
var amount, message;

funcWithRefScalars(amount = [200], message = [null] );
console.log(amount[0] + " - " + message[0]);

function funcWithRefScalars(amount, message) {  // o(amount, message)
   amount[0]  *= 1.2;
   message[0] = "20% increase";
}

在这里,您甚至不需要创建新的虚拟名称,如上面的r


3
"

“全局”JavaScript变量是window对象的成员。你可以将引用作为window对象的成员来访问。

"
var v = "initialized";

function byref(ref) {
  window[ref] = "changed by ref";
}

byref((function(){for(r in window){if(window[r]===v){return(r);}}})());
// It could also be called like... byref('v');
console.log(v); // outputs changed by ref

请注意,上述示例不适用于在函数内声明的变量。

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