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

基本类型(数字,字符串等)按值传递,但是对象是未知的,因为它们都可以按值传递(如果我们认为保存对象的变量实际上是对该对象的引用) )和按引用传递(当我们认为对象的变量包含对象本身时)。

尽管最后并没有什么大不了,但我想知道呈现通过惯例的参数的正确方法是什么。是否有 JavaScript 规范的摘录,其中定义了与此相关的语义?

答案

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无效。
  • 如果该论点是适当的参考,那么一切都会改变。 num100 ,而obj2.item读为"changed"

相反,情况是传入的项目是按值传递的。但是,按值传递的项目本身就是参考。从技术上讲,这称为共享呼叫

实际上,这意味着如果您更改参数本身(如numobj2 ),则不会影响输入该参数的项目。但是,如果您更改参数的INTERNALS ,则它将传播回去(与obj1 )。

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

例:

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

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

无法更改作为参数传递的变量所持有的值,如果 JavaScript 支持通过引用传递,则可以实现。

我的两分钱... 这就是我的理解方式。 (如果我错了,请随时纠正我)

现在该丢掉您所知道的关于按值 / 引用传递的所有信息。

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

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

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

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

现在,让我们说我们更喜欢 object1 ... 因此,我们 “分配” 了一个新变量。

var favoriteObject = object1;

接下来,无论出于何种原因,我们决定我们更喜欢对象 2。因此,我们只需进行一些重新分配。

favoriteObject = object2;

对象 1 或对象 2 均未发生任何事情。我们根本没有更改任何数据。我们所做的只是重新分配了我们最喜欢的对象。重要的是要知道 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;

我们的收藏夹字符串和字符串 1 变量都分配给了 “Hello world”。现在,如果我们想更改我们的收藏夹字符串?会发生什么???

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

呃哦... 发生了什么事。我们无法通过更改 favoriteString 来更改 string1 ... 为什么?因为我们没有更改字符串对象 。我们所做的只是将 “favoriteString” 变量 “重新分配” 为新字符串。这实际上将其与字符串 1 断开连接。在上一个示例中,当我们重命名对象时,我们没有分配任何东西。 (嗯,不是将变量本身分配给...,但是,我们确实将 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 中的函数也意味着赋值。它们是相同的,并且 2 个变量以完全相同的方式连接(也就是说,它们不是一样,除非您算出它们已分配给同一对象)。

“修改变量” 唯一会影响其他变量的时间是基础对象发生突变时(在这种情况下,您尚未修改变量,而是对象本身。

在对象和基元之间进行区分是没有意义的,因为它的工作方式与您没有函数并且只是使用等号分配给新变量的方式相同。

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

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'

考虑以下:

  1. 变量是指向内存中值的指针
  2. 重新分配变量只是将该指针指向一个新值。
  3. 重新分配变量将永远不会影响指向同一对象的其他变量

因此,不要忘记“按引用 / 值传递”,不要因为 “按引用 / 值传递而挂断电话,因为:

  1. 这些术语仅用于描述语言的行为 ,而不一定是实际的基础实现。由于这种抽象,丢失了对体面的解释必不可少的关键细节,这不可避免地导致当前情况,即单个术语不能充分描述实际行为,因此必须提供补充信息
  2. 这些概念最初并不是为了描述 javascript 而定义的,因此当它们只会增加混乱时,我不觉得必须使用它们。

回答您的问题:传递指针。


// 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

函数外部的对象通过提供对外部对象的引用而传递到函数中。

当您使用该引用操纵其对象时,外部对象因此受到影响。但是,如果在函数内部您决定将引用指向其他对象,则根本不会影响外部对象,因为您所做的只是将引用重定向到其他对象。

这样思考:它总是通过价值传递。但是,对象的值不是对象本身,而是对该对象的引用。

这是一个传递数字(原始类型)的示例

function changePrimitive(val) {
    // At this point there are two '10's in memory.
    // Changing one won't affect the other
    val = val * 10;
}
var x = 10;
changePrimitive(x);
// x === 10

对一个对象重复此操作会产生不同的结果:

function changeObject(obj) {
    // At this point there are two references (x and obj) in memory,
    // but these both point to the same object.
    // changing the object will change the underlying object that
    // x and obj both hold a reference to.
    obj.val = obj.val * 10;
}
var x = { val: 10 };
changeObject(x);
// x === { val: 100 }

再举一个例子:

function changeObject(obj) {
    // Again there are two references (x and obj) in memory,
    // these both point to the same object.
    // now we create a completely new object and assign it.
    // obj's reference now points to the new object.
    // x's reference doesn't change.
    obj = { val: 100 };
}
var x = { val: 10 };
changeObject(x);
// x === { val: 10}

“JavaScript:权威指南”这本书的这一章中 ,有一个关于按值复制和传递以及按值进行比较的非常详细的解释。

在离开通过引用操作对象和数组的主题之前,我们需要弄清楚术语的点。

短语 “通过引用” 可以具有多种含义。对某些读者而言,该短语指的是一种函数调用技术,该技术允许函数为其参数分配新值并使这些修改后的值在函数外部可见。这不是本书中使用该术语的方式。

在这里,我们的意思是简单地将对对象或数组的引用(而不是对象本身)传递给函数。函数可以使用引用来修改对象或数组元素的属性。但是,如果函数使用对新对象或数组的引用覆盖了引用,则该修改在函数外部不可见。

熟悉该术语其他含义的读者可能更喜欢说对象和数组是通过值传递的,但是传递的值实际上是引用,而不是对象本身。

JavaScript 总是按值传递 ;一切都是价值类型。

对象是值,对象的成员函数本身就是值(请记住,函数是 JavaScript 中的一流对象)。另外,关于 JavaScript 中的所有内容都是对象的概念;这是错误的。字符串,符号,数字,布尔值,null 和 undefineds 是基元

有时他们可以利用从其基本原型继承的一些成员函数和属性,但这只是为了方便。这并不意味着它们本身就是对象。请尝试以下操作以供参考:

x = "test";
alert(x.foo);
x.foo = 12;
alert(x.foo);

在这两个警报中,您都将找到未定义的值。

在 JavaScript 中,值的类型控制该值是由value-copy还是由reference-copy分配。

原始值始终由 value-copy 分配 / 传递

  • null
  • undefined
  • 布尔值
  • ES6符号

复合值始终由参考副本分配 / 传递

  • 对象
  • 数组
  • 功能

例如

var a = 2;
var b = a; // `b` is always a copy of the value in `a`
b++;
a; // 2
b; // 3

var c = [1,2,3];
var d = c; // `d` is a reference to the shared `[1,2,3]` value
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]

在上面的代码段中,由于2是标量图元,所以a持有该值的一个初始副本,而b则分配了该值的另一个副本。当改变b ,你绝不改变的值a

但是cd都是对相同共享值[1,2,3]单独引用,该共享值是一个复合值。重要的是要注意, cd都不会 “拥有” [1,2,3]值 - 两者都是对该值的同等引用。因此,当使用任何一个引用修改( .push(4) )实际的共享array值本身时,它只会影响一个共享值,并且两个引用都将引用新修改的值[1,2,3,4]

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

// later
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]

当我们使赋值b = [4,5,6] ,我们完全没有采取任何措施来影响a仍在引用的位置( [1,2,3] )。要做到这一点, b必须是一个指向a ,而不是给一个参考array - 但没有这样的能力,JS 的存在!

function foo(x) {
    x.push( 4 );
    x; // [1,2,3,4]

    // later
    x = [4,5,6];
    x.push( 7 );
    x; // [4,5,6,7]
}

var a = [1,2,3];

foo( a );

a; // [1,2,3,4]  not  [4,5,6,7]

当我们在参数中传递a ,它分配的一个拷贝a参考xxa是指向相同[1,2,3]值的单独引用。现在,在函数内部,我们可以使用该引用来改变值本身( push(4) )。但是,当我们使赋值x = [4,5,6] ,这绝不会影响初始参考a指向的位置 - 仍指向[1,2,3,4]值(现在已修改)。

为了通过 value-copy 有效地传递复合值(如array ),您需要手动制作它的副本,以使传递的引用仍然不指向原始值。例如:

foo( a.slice() );

引用副本可以传递的复合值(对象,数组等)

function foo(wrapper) {
    wrapper.a = 42;
}

var obj = {
    a: 2
};

foo( obj );

obj.a; // 42

在这里, obj充当标量基本属性a的包装。当传递给foo(..) ,将传递obj参考的副本并将其设置为wrapper参数。现在,我们可以使用wrapper引用来访问共享库,并更新其属性。函数完成后, obj.a将看到更新后的值42

资源