【问题标题】:Re-initialization array in called function在被调用函数中重新初始化数组
【发布时间】:2016-04-14 23:16:09
【问题描述】:

数组在 javascript 中通过引用传递。这意味着数组是可变的,并且可以在被调用的函数中更改。为什么我无法在被调用函数中重新初始化数组?

python 也是如此

function calling(){
    var arr = [1,2]
    console.log(arr); //prints [1,2]
    called(arr);
    console.log(arr); //prints [1,2,3,4],but I was expecting [5,6]

}

function called(arr){
    arr.push(3)
    arr.push(4)
    arr = [5,6]
}

calling()

更新 我正在寻找(可变性,通过引用传递,通过赋值传递和重新初始化数组)的理由 请不要发布解决方法和问题的解决方案。我知道如何让 arr 打印 [5,6]

【问题讨论】:

  • Javascript 还是 Python?你问的是哪个?您的代码看起来像 Javascript。请将标签修复为一个。
  • 有点不确定你为什么在这里交叉发布:programmers.stackexchange.com/questions/307030/…
  • 代替arr = [5,6],试试arr.splice(0, arr.length, 5, 6)
  • @JörgWMittag - 是的,按值传递,但值是指针或引用。不幸的是,这种技术解释很少能帮助新手真正理解它是如何/为什么起作用的。这个一般问题在这里每隔几周就会出现一次,我们一遍又一遍地看到哪些类型的解释实际上可以帮助新手理解它。对于一个还没有掌握这个概念的新手来说,仅仅说一切都是“按价值传递”永远无法解开这个谜。人们需要比简单的术语更多的解释。
  • 没那么简单。在 Javascript 中,当您拥有 arr = [1,2] 时,arr 包含的是对数组对象的引用。当您执行foo(arr) 时,您传递的是该引用的副本,因此现在您拥有相同引用的两个副本。每一个都指向同一个对象。双方都没有指指点点。所以,“是”它在技术上是按值传递的,但是您传递的是对数组的引用,它是 arr 中引用的副本,而不是对变量本身的引用。从逻辑上讲,我认为它更像是“通过指针传递”,并发现它不那么令人困惑。

标签: javascript python arrays parameter-passing mutable


【解决方案1】:

数组在 javascript 中通过引用传递。

不,他们不是。 ECMAScript/JavaScript 是严格按值传递的。 (更准确地说,共享调用,这是传值的一种特殊情况。)

这意味着数组是可变的,可以在被调用的函数中更改。

不,不是这个意思。它的意思正是它所说的:调用者范围内的对数组的引用作为参数传递给函数,而不是值。

array 是否可变与按引用传递与按值传递无关。 ECMAScript 不是纯粹的函数式语言,大多数对象都可以变异。 Numbers、Symbols 和 Strings 是一个例外。

为什么我无法在被调用函数中重新初始化数组?

您正在尝试修改调用方范围内的引用。这仅适用于传递引用。 ECMAScript 不是通过引用传递的,谁告诉你那是完全错误的。

python 也是如此

在这方面,Python 的行为与 ECMAScript 相同,是的,也是按值传递。

您的困惑源于您错误地认为 ECMAScript/JavaScript 是通过引用传递的,而实际上并非如此。

ECMAScript 使用按值传递,或者更准确地说,是按值传递的一种特殊情况,其中传递的值始终 是一个指针。这种特殊情况有时也称为 call-by-sharing、call-by-object-sharing 或 call-by-object。

Java(用于对象)、C#(默认情况下用于引用类型)、Smalltalk、Python、Ruby 以及几乎所有创建的面向对象语言都使用相同的约定。

注意:某些类型(例如Numbers)实际上是通过值直接传递的,而不是通过中间指针传递的。但是,由于它们是不可变的,在这种情况下,按值传递和按对象共享之间没有可观察到的行为差异,因此您可以通过简单地处理所有内容来大大简化您的心智模型作为按对象共享调用。只需将这些特殊情况解释为您无需担心的内部编译器优化即可。

这是一个简单的示例,您可以运行它来确定 ECMAScript(或任何其他语言,在您翻译后)的参数传递约定:

function isEcmascriptPassByValue(foo) {
  foo.push('More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value!');
  foo = 'No, ECMAScript is pass-by-reference.';
  return;
}

var bar = ['Yes, of course, ECMAScript *is* pass-by-value!'];

isEcmascriptPassByValue(bar);

console.log(bar);
// Yes, of course, ECMAScript *is* pass-by-value!,
// More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value!
def is_python_pass_by_value(foo):
    foo[0] = 'More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value!'
    foo = ['Python is not pass-by-reference.']

quux = ['Yes, of course, Python *is* pass-by-value!']

is_python_pass_by_value(quux)

print(quux[0])
# More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value!

如果你熟悉 C#,这是了解值类型和引用类型的值传递和引用传递之间的区别的一个很好的方法,因为 C# 支持所有 4 种组合:值类型的值(“传统的值传递”),引用类型的值传递(共享调用、对象调用、对象共享调用,如 ECMAScript 中)、传递-引用类型的按引用,值类型的按引用。

(实际上,即使您了解 C#,也并不难理解。)

// In C#, struct defines a value type, class defines a reference type
struct MutableCell
{
    public string value;
}

class Program
{
    // the ref keyword means pass-by-reference, otherwise it's pass-by-value
    // You must explicitly request pass-by-reference both at the definition and the call
    static void IsCSharpPassByValue(string[] foo, MutableCell bar, ref string baz, ref MutableCell qux)
    {
        foo[0] = "More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value.";
        foo = new string[] { "C# is not pass-by-reference." };

        bar.value = "For value types, it is *not* call-by-sharing.";
        bar = new MutableCell { value = "And also not pass-by-reference." };

        baz = "It also supports pass-by-reference if explicitly requested.";

        qux = new MutableCell { value = "Pass-by-reference is supported for value types as well." };
    }

    static void Main(string[] args)
    {
        var quux = new string[] { "Yes, of course, C# *is* pass-by-value!" };

        var corge = new MutableCell { value = "For value types it is pure pass-by-value." };

        var grault = "This string will vanish because of pass-by-reference.";

        var garply = new MutableCell { value = "This string will vanish because of pass-by-reference." };

        // the first two are passed by value, the other two by reference
        IsCSharpPassByValue(quux, corge, ref grault, ref garply);

        Console.WriteLine(quux[0]);
        // More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value.

        Console.WriteLine(corge.value);
        // For value types it is pure pass-by-value.

        Console.WriteLine(grault);
        // It also supports pass-by-reference if explicitly requested.

        Console.WriteLine(garply.value);
        // Pass-by-reference is supported for value types as well.
    }
}

【讨论】:

    【解决方案2】:

    如果你想获得技术,Javascript 真的只是“按值传递”——它不是“按引用传递”。当您传递一个对象(如数组)时,您是按值传递,并且该值是一个引用(对数组的引用的副本,而不是实际数组的副本)。这意味着您可以使用值引用来改变原始对象,但它不是对原始变量的实际引用,因此您不能影响原始变量本身的内容(例如,您不能导致其他变量指向一个新对象)。

    由于这种语言术语实际上并不能解释许多人正在发生的事情,因此下面更多的是对其工作原理的描述:


    当你这样做时:

    arr = [5,6]
    

    在您的函数内部,您只是将一个新数组分配给arr 函数参数。您刚刚指示 Javascript 获取 arr 参数变量并将其指向一个新的数组对象。这对传递给函数的原始数组对象完全没有影响。它仍然快乐地存在于它的另一个生命中。

    您可以像这样更改该数组:

    function called(arr){
        arr.push(3)
        arr.push(4)
        // now set arr back to [5,6]
        arr[0] = 5;
        arr[1] = 6;
        arr.length = 2;
    }
    

    说数组是通过引用传递的有点用词不当,因为它并不是像 C++ 中那样真正的完整引用。有人说它是“通过共享传递”或“通过指针传递”。有些人甚至称其为“按值传递,但值是参考”,这在技术上可能是正确的,但通常不能帮助新手了解发生了什么。在我的大脑中(部分是因为我了解 C/C++),我认为它就像“通过指针传递”。

    虽然 Javascript 实际上没有它自己的指针数据类型,但传递数组的工作方式更像是在 C++ 中传递指针。如果您索引指针,您正在访问原始对象:

    arr[0] = 5;    // this changes the original object
    

    但是,如果你给指针重新赋值:

    arr = [5,6];   // this just makes arr point at a new and different array
    

    然后,您只是让指针变量本身指向一个新对象,而原始对象仍然保持不变。


    所以,总而言之。如果您像arr.push(x) 一样引用arr,那么您正在改变原始数组对象。

    但是,如果您像 arr = [5,6] 那样为 arr 分配一个新数组,那么您只是告诉 Javascript 您希望 arr 变量指向一个新数组。该函数中的 arr 变量现在指向与以前不同的数组。此代码无法再访问原始数组。但是任何其他指向该原始数组的代码仍指向该原始数组。如果您了解 C/C++,它实际上与 C++ 中的指针非常相似。


    这是一个更简单的示例,它不在函数参数的上下文中:

    var x = [1,2];
    var y = x;
    console.log(x);    // [1,2]
    console.log(y);    // [1,2]
    
    y = [3,4];
    console.log(x);    // [1,2]
    console.log(y);    // [3,4]
    

    您只是要求y 现在指向一个新数组 [3,4]。分配y 指向一个新数组完全不会影响原始数组。

    在此示例中,变量 y 类似于 called() 函数中的 arr 函数参数。为其分配新值不会以任何方式更改原始值。

    【讨论】:

    • 我在回答的开头添加了一些信息,说明 JS 在技术上如何通过值传递,而不是通过引用传递。但是对于一个对象来说,传递的值是对对象本身的引用,而不是对原始变量的引用。
    【解决方案3】:

    在 Python 中:

    In [170]: def called(arr):
       .....:     arr.append(3)
       .....:     arr.append(4)
       .....:     arr=[5,6]
       .....:     
    In [171]: arr=[1,2]
    In [172]: called(arr)
    In [173]: arr
    Out[173]: [1, 2, 3, 4]
    

    最初在called 中,arr 是对可变列表[1,2] 的引用。 2 个追加调用会修改该列表。但是arr=[5,6] 步骤重新分配了变量。指向外部arr 的链接已断开,因此没有进一步的更改。

    如果我在调用中添加了 return arr 语句,

    In [175]: called(arr)
    Out[175]: [5, 6]
    In [176]: arr
    Out[176]: [1, 2, 3, 4, 3, 4]
    

    我再次修改全局arr,但返回[5,6]

    called 写出来会更清楚:

    def called(arr):
        arr.append(3)
        arr.append(4)
        res=[5,6]
        return res
    

    最后两行与初始 arr 参数无关,因此重用变量名没有意义。

    要进一步说明发生了什么,请查看对象id 的各个点:

    In [178]: def called(arr):
        arr.append(3)
        arr.append(4)
        print(id(arr))
        arr = [5,6]
       .....:     return arr
       .....: 
    In [179]: id(arr)
    Out[179]: 2999818796
    In [180]: id(called(arr))
    2999818796             # id of global arr
    Out[180]: 2999999564   # id of returned list
    In [181]: id(arr)
    Out[181]: 2999818796
    

    【讨论】:

    • 这很有意义。如果我错过了什么,我会尝试并发布 cmets。谢谢。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-07
    • 2021-12-15
    • 2023-03-09
    • 2016-06-13
    相关资源
    最近更新 更多