JavaScript、Ruby 和 C 不能通过引用调用
🛑 本文是对各种外界文章的回应,这些文章指出 JavaScript 和 Ruby 对于对象是“按引用调用/传递”,对于原语是“按值调用/传递”。
这些文章中有很多提供了许多有价值的信息,本文并非要明确地说这些文章不应该被写出来或毫无用处。相反,本文试图探讨以下概念的语义含义和定义:
- 通过引用调用
- 传递引用
- 引用类型
- 参考
首先,我想发表一些声明,然后我将尝试探究这些声明的实际含义以及我为什么发表这些声明,与外界的各种文章 相反 。
☕ 当你看到这个表情符号 (☕) 时,我尝试用一个非代码的类比来帮助你更好地理解正在发生的事情。这些抽象概念漏洞百出,可能站不住脚,但它们只适用于它们周围段落的上下文。请谨慎对待。
声明
- JavaScript始终是按值调用。
- Ruby始终是按值调用。
- C总是按值调用。
- 这些术语令人困惑,甚至可能有缺陷。
- 该术语仅适用于函数(过程)参数。
- 指针是一个实现细节,它们的存在并不说明函数参数的评估。
历史和定义
我尝试查找上述术语的起源,并且发现早期编程语言中存在相当多的文献。
CPL 的主要特点 (DW Barron 等人,1963):
参数调用有三种模式:按值调用(相当于 ALGOL 的按值调用)、按替换调用(相当于 ALGOL 的按名称调用)和按引用调用。在按引用调用的情况下,实际参数的 LH 值会被传递;这对应于 Strachey 和 Wilkes (1961) 提出的“按简单名称调用”。
值得注意的是,这里的文献讨论了参数调用的模式。它进一步区分了三种模式:call by value
、call by name
和call by reference
。
进一步的文献对这三种策略以及第四种策略(即copy restore
)给出了良好但技术性的定义,该定义发表在《参数传递的语义模型》(Richard E. Fairly,1973)中。我在下面引用了这四个定义中的两个,之后我将对它们进行分解,并用更直观的术语解释它们的含义。
按值调用
[...] 按值调用参数要求在过程调用时对实际参数进行求值。然后,与形式参数关联的内存寄存器将被初始化为该值,并且过程主体中对形式参数的引用将被视为对存储实际参数初始值的本地内存寄存器的引用。由于与实际参数关联的值的副本被复制到本地内存寄存器中,因此过程主体内对参数值的转换与实际参数值是隔离的。由于这种值的隔离,按值调用不能用于将计算值传回调用程序。
粗略地说,这意味着在函数 ( procedure
) 被调用之前,参数会被完全求值。然后,求值的结果会被赋值给函数 ( ) 内部的标识符。在许多编程语言中,这是通过将值复制到formal parameter
第二个内存地址来实现的,这使得函数 ( ) 内部的更改与该函数无关。procedure body
换句话说:原始内存地址的内容(在将求值的表达式传递到函数之前用于存储该内容)不能被函数内部的代码更改,并且函数内部对值的更改不会传播给调用者。
☕ 当你点咖啡时,有人问你的名字,他们可能会记错。这并不会影响你的真实姓名,只会影响到杯子。
通过引用调用
[...] 在“按引用调用”中,过程调用时实际参数的地址(名称)作为与相应形式参数关联的值传递给过程。过程主体中对形式参数的引用会导致通过形式参数寄存器间接寻址引用,指向调用过程中与实际参数关联的内存寄存器。因此,形式参数值的转换会立即传输到调用过程,因为实际参数和形式参数都引用同一个寄存器。
粗略地说,这意味着,就像以前一样,参数会被求值,但与以前不同的是,内存地址( address
/ name
) 会被传递给函数 ( procedure
)。函数内部对参数的更改 ( formal parameter
) 实际上是作用于内存地址,因此会传播回调用者。
☕ 当您前往支持商店维修您的硬件设备并要求维修时,他们可能会给您一个替换设备。这个替换设备仍然属于您,像以前一样属于您,但它可能与您之前维修的设备不完全相同。
引用(和值)类型
这还不是全部。还有一个至关重要的部分,也是造成大部分困惑的原因。现在我来解释一下什么是引用类型,它与参数或函数调用无关。
引用类型和值类型通常是在编程语言如何在内存中存储值的上下文中解释的,这也解释了为什么有些语言同时拥有这两种类型,但整个概念本身就值得写一篇(一系列)文章来阐述。在我看来,维基百科页面的信息量并不大,但它确实引用了各种涉及技术细节的语言规范。
如果一个数据类型在其自身的内存空间中保存数据值,则该数据类型为值类型。这意味着这些数据类型的变量直接包含其值。
与值类型不同,引用类型不直接存储其值,而是存储值所存储的地址。
简而言之,引用类型是指向内存中某个值的类型,而值类型是直接指向其值的类型。
☕ 当您在线付款并输入银行账号信息(例如卡号)时,卡片本身无法更改。但是,银行账户的余额会受到影响。您可以将卡片作为余额的参考(多张卡片可以引用相同的余额)。
☕ 当您线下支付(即现金支付)时,钱会从您的钱包中流出。您的钱包拥有其自身的价值,就像您钱包里的现金一样。价值直接体现在钱包/现金的位置。
给我看看代码证明
function reference_assignment(myRefMaybe) {
myRefMaybe = { key: 42 }
}
var primitiveValue = 1
var someObject = { is: 'changed?' }
reference_assignment(primitiveValue)
primitiveValue
// => 1
reference_assignment(someObject)
// => { is: 'changed?' }
如上所示,someObject
并没有改变,因为它不是 的副本reference
。someObject
根据之前的定义:传递的不是 的内存
地址,而是 的副本。someObject
支持的语言pass by reference
是 PHP,但它需要特殊的语法来改变默认的按值传递方式:
function change_reference_value(&$actually_a_reference)
{
$actually_a_reference = $actually_a_reference + 1;
}
$value = 41;
change_reference_value($value);
// => $value equals 42
我试图保持与 JS 代码相同的语义。
如您所见,PHP 示例实际上改变了输入参数所引用的值。这是因为可以通过参数访问的内存地址。$value
$actually_a_reference
这个命名法有什么问题?
引用类型和“盒装值”使得这一点更加令人困惑,也是我认为命名法可能存在缺陷的原因。
这个术语call-by-value
是有问题的。在 JavaScript 和 Ruby 中,传递的值是引用。这意味着,对装箱原语的引用确实会被复制,因此在函数内部更改原语不会影响外部的原语。这也意味着,对引用类型(例如Array
或Object
)的引用确实会被复制并作为值传递。
因为引用类型引用的是它们的值,所以复制引用类型时,副本仍然引用该值。这也是你所体验到的浅拷贝,而不是深拷贝/克隆。
哇哦。好的。下面是一个探讨这两个概念的例子:
function appendOne(list) {
list.push(1)
}
function replaceWithFive(list) {
list = [5]
}
const first = []
const second = []
appendOne(first)
first
// => [1]
replaceWithFive(second)
second
// => []
在第一个示例中,它输出[1]
,因为该push
方法修改了调用它的对象(该对象是从名称 引用的list
)。这会传播,因为list
参数仍然引用原始对象first
(其引用被复制并作为值传递。list
指向该副本,但指向内存中的相同数据,因为Object
是引用类型)。
第二个例子中,它输出了结果[]
,因为重新赋值操作没有传递到调用者。最终,它并没有重新赋值原始引用,而只是复制了一个引用。
这是另一种记法。👉🏽 表示对内存中不同位置的引用。
first_array = []
second_array = []
first = 👉🏽 first_array
list = copy(first) = 👉🏽 first_array
list.push = (👉🏽 first_array).push(...)
// => (👉🏽 first_array) was changed
second = 👉🏽 second_array
list = copy(second) = 👉🏽 second_array
replace_array = []
list = 👉🏽 replace_array
// => (👉🏽 second_array) was not changed
那么指针呢?
C 语言也始终支持值传递/值调用,但它允许传递指针,从而模拟引用传递。指针是实现细节,例如在 C# 中用于启用引用传递。
然而,在 C 语言中,指针是引用类型!语法*pointer
允许你通过指针找到它的引用。在这段代码的注释中,我尝试解释了底层实现机制。
void modifyParameters(int value, int* pointerA, int* pointerB) {
// passed by value: only the local parameter is modified
value = 42;
// passed by value or "reference", check call site to determine which
*pointerA = 42;
// passed by value or "reference", check call site to determine which
*pointerB = 42;
}
int main() {
int first = 1;
int second = 2;
int random = 100;
int* third = &random;
// "first" is passed by value, which is the default
// "second" is passed by reference by creating a pointer,
// the pointer is passed by value, but it is followed when
// using *pointerA, and thus this is like passing a reference.
// "third" is passed by value. However, it's a pointer and that pointer
// is followed when using *pointerB, and thus this is like
// passing a reference.
modifyParameters(first, &second, third);
// "first" is still 1
// "second" is now 42
// "random" is now 42
// "third" is still a pointer to "random" (unchanged)
return 0;
}
通过共享来呼叫?
一个鲜为人知且使用较少的术语是“共享调用”,它适用于 Ruby、JavaScript、Python、Java 等语言。它意味着所有值都是对象,所有值都被装箱,并且当它们作为值传递时会复制引用。遗憾的是,在文献中,这个概念的使用并不一致,这可能是它鲜为人知或使用的原因。
就本文的目的而言,call-by-sharing 是call by value
,但其值始终是一个参考。
结论
简而言之:它始终按值传递,但变量的值是引用。所有原始方法都会返回一个新值,因此无法修改它;所有对象和数组都可以拥有修改其值的方法,因此可以修改它。
在使用 的语言中,您无法直接影响参数的内存地址call-by-value
,但您可以影响参数所引用的内容。也就是说,您可以影响参数指向的内存。
声明“原始数据类型通过值传递,对象通过引用传递。”是不正确的。
文章来源:https://dev.to/xpbytes/javascript-ruby-and-c-are-not-call-by-reference-23f7