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

1756

原始类型(例如数字、字符串等)通过值传递,但对象不同,因为它们既可以按值传递(在这种情况下,我们认为保存对象的变量实际上是对该对象的引用),也可以按引用传递(当我们认为变量对对象本身持有该对象时)。

虽然最终并不重要,但我想知道呈现参数传递约定的正确方式是什么。是否有来自JavaScript规范的节选,定义了关于此事应该是什么语义?


8
我认为你不小心颠倒了传值和传引用的定义... "传值(如果我们认为一个变量持有一个对象实际上是指向该对象的引用),传引用(当我们认为变量持有对象本身时)" - Niko Bellic
11
是的。无论在任何编程语言中的函数调用中语法如何,传递引用都表示传递的变量所关联的数据在传递到函数时不会被复制,因此函数对传递的变量所做的任何修改将在函数调用终止后保留在程序中。传值则意味着将变量相关的数据实际上复制到函数中传递,当变量超出函数体作用域并且函数返回时,该函数对变量所做的任何修改都将丢失。 - John Sonderson
9
这个老问题有点棘手,因为它的得票最高的答案是错误的。JavaScript 严格来说是“按值传递”。 - Pointy
9
术语令人遗憾地令人困惑。事实上,“传值”和“传引用”这些术语比许多更现代的编程语言特性还要早。 “值”和“引用”这两个词 具体 指函数调用表达式中的参数。JavaScript始终在调用函数之前计算函数调用参数列表中的每个表达式,因此参数始终是值。令人困惑的是,对象的引用是常见的JavaScript值。然而,这并不意味着它是一种“传引用”的语言。 - Pointy
4
“按引用传递”特指以下情况:如果你有var x=3, y=x; f(x); alert(y === x);,那么函数f()可以使得alert报告false而不是true。在JavaScript中,这是不可能的,所以它不是“按引用传递”。能够传递可修改对象的引用是好的,但这并不是“按引用传递”的意思。就像我说的,术语混淆真是令人遗憾。 - Pointy
显示剩余12条评论
35个回答

2
对于编程语言专家来说,我已经阅读了ECMAScript 5.1的以下章节(比最新版本更易于阅读),并且深入到如下面的问题,并在ECMAScript邮件列表上询问了它。 总之:一切都是按值传递的,但是对象的属性是引用,而标准中"对象"的定义相当缺乏。

参数列表的构建

第11.2.4节“参数列表”对只包含一个参数的参数列表产生的情况如下所述:

产生AssignmentExpression的评估结果ref。

  1. 让ref成为评估AssignmentExpression的结果。
  2. 让arg成为GetValue(ref)。
  3. 返回一个仅包含arg的列表。
该部分还列举了参数列表有0或>1个参数的情况。
因此,所有的东西都是按引用传递的。

对象属性的访问

第11.2.1节“属性访问者”

产生MemberExpression : MemberExpression [ Expression ]的评估结果:

  1. 让baseReference成为评估MemberExpression的结果。
  2. 让baseValue成为GetValue(baseReference)。
  3. 让propertyNameReference成为评估Expression的结果。
  4. 让propertyNameValue成为GetValue(propertyNameReference)。
  5. 调用CheckObjectCoercible(baseValue)。
  6. 让propertyNameString成为ToString(propertyNameValue)。
  7. 如果正在评估的语法产生包含在严格模式代码中,则让strict为true,否则让strict为false。
  8. 返回类型为Reference的值,其基值为baseValue,引用名称为propertyNameString,其严格模式标志为strict。
因此,对象的属性始终可以作为引用使用。

关于引用

在第8.7节"The Reference Specification Type "中描述了引用不是语言中的真实类型-它们仅用于描述删除,typeof和赋值运算符的行为。

"Object"的定义

5.1版中定义"对象是一组属性"。因此,我们可以推断出,对象的值是集合,但是在规范中对集合的值进行了很差的定义,需要一些努力来理解。

让我惊讶的是有多少人会混淆按值传递参数、按引用传递参数、整个对象的操作和对它们属性的操作之间的区别。 1979年,我并没有在计算机科学方面获得学位,而是选择在MBA课程中增加了大约15个小时的计算机科学选修课程。 然而,很快就变得清楚,我的这些概念的理解至少与拥有计算机科学或数学学位的任何同事持平。 学习汇编语言,这些概念将变得非常清晰。 - David A. Gray
规范中的引用与所讨论的行为无关。它是一个中间构造,用于解释为什么 a.b = 1 能够知道在哪个对象(a)上设置属性(b)(因为 a.b 会被求值为 Reference { a, "b" })。 - Jonas Wilms

1

我找到的最简洁的解释在AirBNB样式指南中:

  • 基元类型:当您访问基元类型时,您直接使用其值

    • 字符串
    • 数字
    • 布尔值
    • null
    • 未定义

E.g.:

var foo = 1,
    bar = foo;

bar = 9;

console.log(foo, bar); // => 1, 9
  • 复杂类型:当您访问复杂类型时,您是在引用其值上进行操作的

    • 对象
    • 数组
    • 函数

E.g.:

var foo = [1, 2],
    bar = foo;

bar[0] = 9;

console.log(foo[0], bar[0]); // => 9, 9

即,基本类型按值传递,复杂类型按引用传递。

不,所有东西都是按值传递的。这只取决于你传递的是什么(一个值还是一个引用)。请参见此链接 - Scott Marcus

0

我已经多次阅读了这些答案,但直到我了解了由芭芭拉·利斯科夫(Barbara Liskov)所称的"共享调用"的技术定义后,才真正理解它。

共享调用的语义与按引用调用不同,因为函数内部对函数参数的赋值对调用者不可见(不像按引用调用语义)[需要引证],因此,例如,如果传递了一个变量,则无法在调用者的范围内模拟对该变量的赋值。然而,由于函数可以访问与调用者相同的对象(不进行复制),因此如果对象是可变的,则函数内部对这些对象的更改对调用者是可见的,这可能看起来与按值调用语义不同。在函数内部对可变对象的更改对调用者是可见的,因为对象没有被复制或克隆-它是共享的。

也就是说,如果您访问参数值本身,则可以更改参数引用。另一方面,对参数的赋值将在评估后消失,并且对函数调用者不可访问。


不,一个对象是否可变并不是真正的问题。一切都是按值传递的。这只取决于你传递的是什么(值还是引用)。请参见此链接 - Scott Marcus
她所描述的是通过值传递引用。没有必要引入新术语。 - Sanjeev

0
function passByCopy ([...array], {...object})
{
   console .log ("copied objects", array, object)
}

passByCopy ([1,2,3], {a:1, b:2, c:3})

function passByReference (array, object)
{
   console .log ("same objects", array, object)
}

passByReference ([1,2,3], {a:1, b:2, c:3})

1
你应该添加解释。 - Super Kai - Kazuya Ito
1
你应该添加解释。 - undefined

0
引用提问者的话:
原始类型(数字、字符串等)是按值传递的,但对象[...$可以既按值传递(在这种情况下,我们认为持有对象的变量实际上是对对象的引用),也可以按引用传递(在这种情况下,我们认为变量对对象本身进行持有)。
这是不正确的。对象作为参数传递只有一种方式。虽然其他答案也区分原始值和非原始值,但这是一个干扰和无关紧要的问题。
没有按引用传递
考虑以下代码,它不关心a是原始类型还是对象:
function alter(arg) {  /* some magic happening here to `arg` */ }

function main() {
    var a, b;
    a = b = { x: 1 }; // Example value. It can be an object or primitive: irrelevant here
    console.log(a === b); // true
    alter(a);
    console.log(a === b); // false? (not possible)
}

如果函数alter有一种方法可以使第二个输出false,那么我们可以说是按引用传递,因为arg将成为a的别名。但在JavaScript中,这是不可能的。原始类型的概念与此无关:在JavaScript中不存在按引用传递。
对象
在JavaScript中,对象是对属性(和槽位)的引用。当一个函数以对象作为参数调用时,这并不会创建一个新的属性集合。函数的参数变量(即局部变量)将接收相同的值(即属性集合的引用)。
变量赋值与对象变异
调用者的数据结构可能会被变异,也可以通过函数调用来实现。这显然只对可变值(根据定义)成立,在JavaScript中,这是通过将值赋给属性来实现的。有两件事需要区分:
  • 对对象属性的赋值改变了调用者和被调用者都引用的对象。
  • 对参数变量的赋值不会改变调用者的数据。特别是,如果该参数变量是一个对象(一个引用),那么该引用将被覆盖,因此函数将与调用者的数据分离

-1

我发现Underscore.js库扩展方法非常有用,当我想将一个对象作为参数传递时,该对象可能会被修改或完全替换。

function replaceOrModify(aObj) {
  if (modify) {

    aObj.setNewValue('foo');

  } else {

   var newObj = new MyObject();
   // _.extend(destination, *sources) 
   _.extend(newObj, aObj);
  }
}

-1
在低级语言中,如果您想通过引用传递变量,则必须在函数的创建中使用特定的语法:
int myAge = 14;
increaseAgeByRef(myAge);
function increaseAgeByRef(int &age) {
  *age = *age + 1;
}

&age 是对 myAge 的引用,但如果你想要值,你必须使用 *age 来转换引用。

JavaScript 是一种高级语言,它会为你执行这个转换。

因此,尽管对象是按引用传递的,但语言会将引用参数转换为值。你不需要在函数定义上使用 & 来按引用传递它,也不需要在函数体上使用 * 来将引用转换为值,JavaScript 会为你执行这些操作。

这就是为什么当你尝试在函数内部更改对象时,通过替换它的值(例如 age = {value:5}),更改不会持久化,但如果你更改它的属性(例如 age.value = 5),它会。

了解更多


不需要(或不允许)在C++中取消引用引用。它要么是解引用指针,要么是不解引用引用。 - Peter Mortensen
这个答案事实上是错误的。JavaScript 不进行这样的转换。在 JavaScript 中无法通过引用传递。"按引用传递" 的整个意义在于函数能够更改 myAge 的值。在 JavaScript 中你做不到这一点。你可以更改 myAge 引用的对象的属性,但你不能更改变量本身 myAge。这就是 "按引用传递" 的含义,即能够更改函数外部变量的值。 - gman

-1
如果您想要像其他语言一样的(传递值的副本)正常函数参数行为, 那么在将对象传递到函数之前,只需克隆该对象即可:
function run()
{
    var test = [];
    test.push(1);

    console.log('before: '+test); // 1

    changeVariable(_.clone(test)); // (Note: I am using lodash _.clone() function)
 
    console.log('after: '+test); // 1 
}


function changeVariable(test2) {
  var test1 = test2;
  test1.push(2); 
  console.log('inside func:', test1);  // inside func: [1,2]
}   


run();    

-3
  1. 原始类型(数字、布尔值等)按值传递。
    • 字符串是不可变的,所以对它们来说并不重要。
  2. 对象按引用传递(引用按值传递)。

不,所有东西都是按值传递的。这只取决于你传递的是什么(一个值还是一个引用)。请参见此链接 - Scott Marcus
你的第二个语句自相矛盾。 - Jörg W Mittag

-3

我认为这是传递副本 -

考虑到参数和变量对象是在函数调用开始时创建的执行上下文中创建的对象 - 您实际传递给函数的值/引用只是存储在这些参数+变量对象中。

简单地说,对于原始类型,值在函数调用开始时被复制,对于对象类型,引用被复制。


1
"pass-by-copy" === 按值传递 - Scott Marcus

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