JavaScript 是按引用传递还是按值传递语言?

IT技术 javascript pass-by-reference pass-by-value
2021-01-06 21:54:40

原始类型(数字、字符串等)按值传递,但对象是未知的,因为它们都可以按值传递(以防我们认为持有对象的变量实际上是对对象的引用) 和按引用传递(当我们认为对象的变量保存对象本身时)。

尽管最后并不重要,但我想知道通过约定呈现参数的正确方法是什么。是否有 JavaScript 规范的摘录,它定义了关于此的语义应该是什么?

6个回答

这在 JavaScript 中很有趣。考虑这个例子:

function changeStuff(a, b, c)
{
  a = a * 10;
  b.item = "changed";
  c = {item: "changed"};
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);
console.log(obj1.item);
console.log(obj2.item);

这会产生输出:

10
changed
unchanged
  • 如果obj1根本不是引用,则更改obj1.item不会obj1对函数外部产生影响
  • 如果参数是一个正确的引用,那么一切都会改变。num100obj2.item会读"changed"相反,num留下10obj2.item保持"unchanged“。

相反,情况是传入的项目是按值传递的。但是按值传递的项目本身就是一个引用。从技术上讲,这称为共享调用

实际上,这意味着如果您更改参数本身(如使用numobj2),则不会影响输入参数的项目。但是,如果您更改参数内部结构,它将向上传播(与 一样obj1)。

我认为这也用于 Java:按值引用。
2021-02-07 21:54:40
虽然这个答案是最受欢迎的,但它可能有点令人困惑,因为它指出“如果它是纯粹的按值传递”。JavaScript纯粹的值传递。但是传递的值是一个引用。这根本不限于参数传递。您可以简单地复制变量 byvar obj1 = { item: 'unchanged' }; var obj2 = obj1; obj2.item = 'changed';并观察到与您的示例相同的效果。因此我个人参考了 Tim Goodman 的回答
2021-02-13 21:54:40
这与 C# 完全相同(或至少在语义上)。对象有两种类型:值(原始类型)和引用。
2021-02-14 21:54:40
真正的原因是在 changeStuff 中,num、obj1 和 obj2 是引用。当您更改itemobj1 引用的对象属性时,您正在更改最初设置为“未更改”的项目属性的值。当您为 obj2 分配 {item: "changed"} 值时,您正在更改对新对象的引用(该对象在函数退出时立即超出范围)。如果您将函数参数命名为 numf、obj1f 和 obj2f 之类的东西,则会更加明显地看出正在发生的事情。然后您会看到参数隐藏了外部变量名称。
2021-02-18 21:54:40
@BartoNaz 不是真的。你想要的是通过引用传递引用,而不是通过值传递引用。但是 JavaScript 总是按值传递引用,就像它按值传递其他所有内容一样。(为了比较,C# 具有类似于 JavaScript 和 Java 的按值传递行为,但允许您使用ref关键字指定按引用传递。)通常您只会让函数返回新对象,然后执行调用函数时的赋值。例如,foo = GetNewFoo();而不是GetNewFoo(foo);
2021-02-26 21:54:40

它总是按值传递,但对于对象,变量的值是一个引用。因此,当您传递一个对象并更改其成员时,这些更改会在函数之外持续存在。这使它看起来像通过引用传递。但是如果你真的改变了对象变量的值,你会看到改变不会持续存在,证明它确实是按值传递的。

例子:

function changeObject(x) {
  x = { member: "bar" };
  console.log("in changeObject: " + x.member);
}

function changeMember(x) {
  x.member = "bar";
  console.log("in changeMember: " + x.member);
}

var x = { member: "foo" };

console.log("before changeObject: " + x.member);
changeObject(x);
console.log("after changeObject: " + x.member); /* change did not persist */

console.log("before changeMember: " + x.member);
changeMember(x);
console.log("after changeMember: " + x.member); /* change persists */

输出:

before changeObject: foo
in changeObject: bar
after changeObject: foo

before changeMember: foo
in changeMember: bar
after changeMember: bar
@daylight:实际上,你错了;如果它是由 const ref 传递的,尝试执行 changeObject 将导致错误,而不仅仅是失败。尝试为 C++ 中的 const 引用分配一个新值,编译器拒绝它。就用户而言,这就是按值传递和按常量引用传递之间的区别。
2021-02-09 21:54:40
换句话说,这里令人困惑的不是按值传递/按引用传递。一切都是传值,句号。令人困惑的是,您不能传递对象,也不能将对象存储在变量中。每次您认为这样做时,您实际上是在传递或存储对该对象的引用。但是当你去访问它的成员时,会发生一个无声的解引用,它使你的变量保存实际对象的虚构永久化。
2021-02-16 21:54:40
@adityamenon 很难回答“为什么”,但我注意到 Java 和 C# 的设计者做出了类似的选择;这不仅仅是一些 JavaScript 的怪异之处。真的,它是非常一致的按值传递,让人们感到困惑的事情是一个值可以是一个引用。这与在 C++ 中(按值)传递指针然后取消引用它以设置成员没有太大区别。没有人会对这种变化持续存在感到惊讶。但是因为这些语言抽象出指针并默默地为您取消引用,所以人们会感到困惑。
2021-03-03 21:54:40
@daylight:这不是恒定的参考。在 中changeObject,我已更改x为包含对新对象的引用。 顺便说一下,x = {member:"bar"};相当于x = new Object(); x.member = "bar";我所说的对 C# 也是如此。
2021-03-04 21:54:40
@daylight:对于C#,可以从函数外部看到这一点,如果使用ref关键字可以通过引用传递引用(而不是默认通过值传递引用),然后指向a的更改new Object() 持续存在.
2021-03-04 21:54:40

变量不“持有”对象;它包含一个引用。您可以将该引用分配给另一个变量,现在两者都引用同一个对象。它总是按值传递(即使该值是引用......)。

没有办法改变作为参数传递的变量所持有的值,如果 JavaScript 支持按引用传递,这将是可能的。

短语“按值传递引用”似乎不必要地混淆和多余。当传递一个引用时,当然必须传递一些值虽然在技术上是正确的,但除非另有说明,否则大多数人的默认假设是任何东西都是按值传递的。所以当然引用是按值传递的,除非它本身是按引用传递的(有点像 C 中指向指针的指针),但在这种情况下,Javascript 甚至不支持它,所以我认为它无助于使概念更清晰
2021-02-11 21:54:40
这让我有点困惑。不是通过引用传递引用吗?
2021-02-16 21:54:40
作者的意思是,通过传递一个引用,你传递了一个引用值(另一种思考方式是传递内存地址的值)。所以这就是为什么如果您重新声明对象,原始对象不会改变,因为您正在不同的内存位置创建一个新对象。如果更改属性,则原始对象会更改,因为您在原始内存位置(未重新分配)更改了它。
2021-02-24 21:54:40
简单地说,十年后,引用是按值复制的。
2021-02-28 21:54:40
与 JavaScript 的混淆点在于它在这件事上别无选择,@geg:复杂类型总是被间接处理,简单类型总是直接处理。无法获得对整数的引用,也无法阻止传递对元组的引用。这......有时会很尴尬。
2021-03-06 21:54:40

我的两分钱……我是这么理解的。(如果我错了,请随时纠正我)

是时候扔掉你所知道的关于按值/引用传递的所有内容了。

因为在 JavaScript 中,它是按值传递还是按引用或其他方式传递都无关紧要。重要的是传递给函数的参数的变异与分配。

好的,让我尽力解释我的意思。假设您有几个对象。

var object1 = {};
var object2 = {};

我们所做的是“赋值”...我们已经将 2 个单独的空对象分配给变量“object1”和“object2”。

现在,假设我们更喜欢 object1...所以,我们“分配”一个新变量。

var favoriteObject = object1;

接下来,无论出于何种原因,我们决定更喜欢对象 2。所以,我们做一些重新分配。

favoriteObject = object2;

object1 或 object2 什么也没发生。我们根本没有更改任何数据。我们所做的只是重新分配我们最喜欢的对象。重要的是要知道 object2 和 favoriteObject 都分配给同一个对象。我们可以通过这些变量中的任何一个来更改该对象。

object2.name = 'Fred';
console.log(favoriteObject.name) // Logs Fred
favoriteObject.name = 'Joe';
console.log(object2.name); // Logs Joe

好的,现在让我们看看像字符串这样的原语

var string1 = 'Hello world';
var string2 = 'Goodbye world';

再次,我们选择一个最喜欢的。

var favoriteString = string1;

我们的 favoriteString 和 string1 变量都被分配给了“Hello world”。现在,如果我们想改变我们的收藏夹怎么办???会发生什么???

favoriteString = 'Hello everyone';
console.log(favoriteString); // Logs 'Hello everyone'
console.log(string1); // Logs 'Hello world'

哦哦……发生了什么事。我们不能通过改变 favoriteString 来改变 string1 ......为什么??因为我们没有改变我们的字符串对象我们所做的只是将 favoriteString变量“重新分配”给一个新字符串。这基本上将它与 string1 断开了连接。在前面的例子中,当我们重命名对象时,我们没有分配任何东西。(好吧,不是给变量本身,......然而,我们确实将 name 属性分配给了一个新字符串。)相反,我们改变了对象,它保持了 2 个变量和底层对象之间的连接。(即使我们想修改或改变字符串对象本身,我们也做不到,因为字符串在 JavaScript 中实际上是不可变的。)

现在,转到函数和传递参数......当你调用一个函数并传递一个参数时,你本质上做的是对一个新变量的“赋值”,它的工作原理与你使用等号 (=)。

以这些例子为例。

var myString = 'hello';

// Assign to a new variable (just like when you pass to a function)
var param1 = myString;
param1 = 'world'; // Re assignment

console.log(myString); // Logs 'hello'
console.log(param1);   // Logs 'world'

现在,同样的事情,但有一个功能

function myFunc(param1) {
    param1 = 'world';

    console.log(param1);   // Logs 'world'
}

var myString = 'hello';
// Calls myFunc and assigns param1 to myString just like param1 = myString
myFunc(myString);

console.log(myString); // logs 'hello'

好的,现在让我们举几个使用对象的例子......首先,没有函数。

var myObject = {
    firstName: 'Joe',
    lastName: 'Smith'
};

// Assign to a new variable (just like when you pass to a function)
var otherObj = myObject;

// Let's mutate our object
otherObj.firstName = 'Sue'; // I guess Joe decided to be a girl

console.log(myObject.firstName); // Logs 'Sue'
console.log(otherObj.firstName); // Logs 'Sue'

// Now, let's reassign the variable
otherObj = {
    firstName: 'Jack',
    lastName: 'Frost'
};

// Now, otherObj and myObject are assigned to 2 very different objects
// And mutating one object has no influence on the other
console.log(myObject.firstName); // Logs 'Sue'
console.log(otherObj.firstName); // Logs 'Jack';

现在,同样的事情,但有一个函数调用

function myFunc(otherObj) {

    // Let's mutate our object
    otherObj.firstName = 'Sue';
    console.log(otherObj.firstName); // Logs 'Sue'

    // Now let's re-assign
    otherObj = {
        firstName: 'Jack',
        lastName: 'Frost'
    };
    console.log(otherObj.firstName); // Logs 'Jack'

    // Again, otherObj and myObject are assigned to 2 very different objects
    // And mutating one object doesn't magically mutate the other
}

var myObject = {
    firstName: 'Joe',
    lastName: 'Smith'
};

// Calls myFunc and assigns otherObj to myObject just like otherObj = myObject
myFunc(myObject);

console.log(myObject.firstName); // Logs 'Sue', just like before

好的,如果你通读了整篇文章,也许你现在对 JavaScript 中的函数调用是如何工作的有了更好的理解。通过引用传递还是通过值传递并不重要……重要的是赋值与变异。

每次将变量传递给函数时,您都在“分配”参数变量的名称,就像使用等号 (=) 一样。

永远记住等号 (=) 表示赋值。永远记住,在 JavaScript 中将参数传递给函数也意味着赋值。它们是相同的,并且这两个变量以完全相同的方式连接(也就是说它们不是,除非您将它们分配给同一个对象)。

“修改变量”影响不同变量的唯一时间是底层对象发生变异时(在这种情况下,您没有修改变量,而是修改了对象本身。

区分对象和基元是没有意义的,因为它的工作方式与您没有函数而只是使用等号分配给新变量的方式完全相同。

唯一的问题是当您传递给函数的变量名称与函数参数名称相同时。发生这种情况时,您必须将函数内部的参数视为函数私有的全新变量(因为它是)

function myFunc(myString) {
    // myString is private and does not affect the outer variable
    myString = 'hello';
}

var myString = 'test';
myString = myString; // Does nothing, myString is still 'test';

myFunc(myString);
console.log(myString); // Logs 'test'
我发布了另一个带有传统定义的答案,希望能减少混淆。“按值传递”和“按引用传递”的传统定义是在自动取消引用之前的内存指针时代定义的。很清楚,对象变量的值实际上是内存指针位置,而不是对象。尽管您对赋值与变异的讨论可能很有用,但没有必要抛弃传统术语及其定义。变异、赋值、传值、传引用等不能相互矛盾。
2021-02-08 21:54:40
现在我明白 obj1 = obj2 意味着 obj1 和 obj2 现在都指向相同的引用位置,如果我修改 obj2 的内部结构,引用 obj1 将暴露相同的内部结构。如何复制对象,以便在执行source = { "id":"1"}; copy = source /*this is wrong*/; copy.id="2"该源时仍然是 {"id":"1"}?
2021-02-14 21:54:40
这是正确的-条款传递的value传递通过引用在编程语言设计的意义,这些意义什么都没有做对象突变。这都是关于函数参数如何工作的。
2021-02-15 21:54:40
“数字”也是“不可变的”吗?
2021-02-15 21:54:40
对于任何 C 程序员,想想 char*。 foo(char *a){a="hello";} 什么都不做,但如果你这样做,foo(char *a){a[0]='h';a[1]='i';a[2]=0;}它会在外面改变,因为它是a通过引用字符串(字符数组)的值传递的内存位置。允许在 C 中按值传递结构(类似于 js 对象),但不推荐。JavaScript 只是强制执行这些最佳实践,并隐藏了不必要的和通常不受欢迎的杂物……它确实使阅读更容易。
2021-02-18 21:54:40

这些短语/概念最初是在 JS 被创建之前很久就被定义的,它们并没有准确地描述 javascript 的语义。我认为尝试将它们应用于 JS 会导致更多的混乱。

所以不要纠结于“通过引用/值传递”。

考虑以下:

  1. 变量是指向值的指针
  2. 重新分配一个变量只是将该指针指向一个新值。
  3. 重新分配变量永远不会影响指向同一对象的其他变量,因为每个变量都有自己的指针。

所以如果我必须给它一个名字,我会说“传递指针” ——我们不处理 JS 中的指针,但底层引擎会处理。


// code
var obj = {
    name: 'Fred',
    num: 1
};

// illustration
               'Fred'
              /
             /
(obj) ---- {}
             \
              \
               1

// code
obj.name = 'George';


// illustration
                 'Fred'


(obj) ---- {} ----- 'George'
             \
              \
               1

// code
obj = {};

// illustration
                 'Fred'


(obj)      {} ----- 'George'
  |          \
  |           \
 { }            1

// code
var obj = {
    text: 'Hello world!'
};

/* function parameters get their own pointer to 
 * the arguments that are passed in, just like any other variable */
someFunc(obj);


// illustration
(caller scope)        (someFunc scope)
           \             /
            \           /
             \         /
              \       /
               \     /
                 { }
                  |
                  |
                  |
            'Hello world'

最后的一些评论:

  • 短语“按值/引用传递”仅用于描述语言行为,不一定是实际的底层实现。由于这种抽象,对于一个体面的解释必不可少的关键细节丢失了,这不可避免地导致当前的情况,即在没有附加信息的情况下,单个术语不能充分描述实际行为。
  • 人们很容易认为基元是由特殊规则强制执行的,而对象不是,但基元只是指针链的末端。
  • 作为最后一个示例,请考虑为什么清除数组的常见尝试没有按预期工作。

var a = [1,2];
var b = a;

a = [];
console.log(b); // [1,2]
// doesn't work because `b` is still pointing at the original array
没有必要忘记“通过引用/值传递”这些术语具有准确描述您试图描述的内容的历史含义。如果我们抛弃历史术语和定义,变得懒于了解它们的原始含义,那么我们就失去了代际有效沟通的能力。没有好的方法讨论不同语言和系统之间的差异。相反,新程序员需要学习和理解传统术语以及它们的来源和原因。否则,我们会集体失去知识和理解。
2021-02-07 21:54:40
@MichaelHoffmann2)关于类似的东西var a = b,javascript 不提供使用指针的机制,因此变量永远不能指向指针(就像在 C 中一样),尽管底层 javascript 引擎无疑使用它们。所以......var a = b将指向a“正确操作数的指针”
2021-02-11 21:54:40
我在这里问了问题 #1(特别是关于 Chrome,因为每个浏览器的实现可能不同)stackoverflow.com/q/42778439/539997,我仍然在想如何表达问题 #2。任何帮助表示赞赏。
2021-02-14 21:54:40
额外学分的后续问题 ;) 垃圾收集如何工作?如果我通过一百万个{'George', 1}循环变量,但一次只使用其中一个,那么其他的如何管理?当我将一个变量赋给另一个变量的值时会发生什么?那么我是指向一个指针,还是指向右操作数的指针?是否var myExistingVar = {"blah", 42}; var obj = myExistingVar;在结果obj指向{"blah", 42},还是myExistingVar
2021-02-25 21:54:40
@MichaelHoffmann 这些应该有他们自己的 SO 问题,并且可能已经得到了比我能管理的更好的回答。话虽如此,1)我在浏览器开发工具中为循环函数运行了一个内存配置文件,例如您描述的那个,并且在整个循环过程中看到了内存使用量的峰值。这似乎表明在循环的每次迭代中确实正在创建新的相同对象。当尖峰突然下降时,垃圾收集器只是清理了一组这些未使用的对象。
2021-03-01 21:54:40