原始类型(数字、字符串等)按值传递,但对象是未知的,因为它们都可以按值传递(如果我们认为持有对象的变量实际上是对对象的引用) 和通过引用传递(当我们认为对象的变量包含对象本身时)。
尽管最后并不重要,但我想知道呈现通过约定的参数的正确方法是什么。是否有 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
num
会100
,obj2.item
会读"changed"
。相反,num
留下10
并obj2.item
保持"unchanged
“。相反,情况是传入的项目是按值传递的。但是按值传递的项目本身就是一个引用。从技术上讲,这称为共享呼叫。
实际上,这意味着如果您更改参数本身(如num
and obj2
),这不会影响输入参数的项目。但是,如果您更改参数的内部结构,它将传播回来(与 一样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 = {};
我们所做的是“赋值”...我们已经将 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 怎么办???会发生什么???
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 中将参数传递给函数也意味着赋值。它们是相同的,并且 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'
这些短语/概念最初是在创建 JS 之前很久就定义的,它们不能准确地描述 javascript 的语义。我认为尝试将它们应用于 JS 会导致更多的混乱。
所以不要沉迷于“按引用/值传递”。
考虑以下:
所以如果我必须给它一个名字,我会说“pass-by-pointer” ——我们不处理 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
通过提供对外部对象的引用,将函数外部的对象传递给函数。
当您使用该引用来操作其对象时,外部的对象就会受到影响。但是,如果在函数内部您决定将引用指向其他对象,则根本不会影响外部对象,因为您所做的只是将引用重新指向其他对象。
可以这样想:它总是按值传递。但是,对象的值不是对象本身,而是对该对象的引用。
这是一个示例,传递一个数字(原始类型)
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 中的一切都是对象的概念;这是错误的。字符串、符号、数字、布尔值、空值和未定义值都是原语。
有时他们可以利用从其基本原型继承的一些成员函数和属性,但这只是为了方便。这并不意味着它们本身就是对象。请尝试以下方法以供参考:
x = "test";
console.log(x.foo);
x.foo = 12;
console.log(x.foo);
在两者中console.log
,您都会发现值是undefined
。
在 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
.
但两者c
和d
都是对相同共享值的单独引用[1,2,3]
,这是一个复合值。重要的是要注意,既不“拥有”该值,c
也d
不再“拥有”该[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
x
x
a
[1,2,3]
push(4)
x = [4,5,6]
a
[1,2,3,4]
要通过值复制有效地传递复合值(如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
。
好吧,它是关于“性能”和“速度”以及编程语言中的简单单词“内存管理”。
在 javascript 中,我们可以将值放在两层中:type1 -objects
和type2 - 所有其他类型的值,例如string
& boolean
& 等
如果您将内存想象为以下正方形,其中每个正方形中只能保存一个 type2-value:
每个 type2-value(绿色)是一个正方形,而 type1-value(蓝色)是它们的一组:
关键是,如果你想指示一个 type2-value,地址是简单的,但是如果你想对 type1-value 做同样的事情,那就不容易了!:
在一个更复杂的故事中:
虽然这里的绿色箭头是一个典型变量,紫色是一个对象变量,所以因为绿色箭头(典型变量)只有一个任务(并且表示典型值),我们不需要将它的值与它所以我们将绿色箭头与它的值一起移动到任何地方以及所有分配、函数等中......
但是我们不能用紫色箭头做同样的事情,我们可能想在这里移动“约翰”单元格或许多其他事情......,所以紫色箭头会粘在它的位置,只有分配给它的典型箭头会移动...
一个非常令人困惑的情况是您无法意识到引用的变量是如何变化的,让我们看一个非常好的示例:
let arr = [1, 2, 3, 4, 5]; //arr is an object now and a purple arrow is indicating it
let obj2 = arr; // now, obj2 is another purple arrow that is indicating the value of arr obj
let obj3 = ['a', 'b', 'c'];
obj2.push(6); // first pic below - making a new hand for the blue circle to point the 6
//obj2 = [1, 2, 3, 4, 5, 6]
//arr = [1, 2, 3, 4, 5, 6]
//we changed the blue circle object value (type1-value) and due to arr and obj2 are indicating that so both of them changed
obj2 = obj3; //next pic below - changing the direction of obj2 array from blue circle to orange circle so obj2 is no more [1,2,3,4,5,6] and it's no more about changing anything in it but we completely changed its direction and now obj2 is pointing to obj3
//obj2 = ['a', 'b', 'c'];
//obj3 = ['a', 'b', 'c'];
这是对按值传递和按引用传递 (JavaScript) 的更多解释。在这个概念中,他们谈论的是通过引用传递变量和通过引用传递变量。
按值传递(原始类型)
var a = 3;
var b = a;
console.log(a); // a = 3
console.log(b); // b = 3
a=4;
console.log(a); // a = 4
console.log(b); // b = 3
通过引用传递(对象)
var c = { "name" : "john" };
var d = c;
console.log(c); // { "name" : "john" }
console.log(d); // { "name" : "john" }
c.name = "doe";
console.log(c); // { "name" : "doe" }
console.log(d); // { "name" : "doe" }
c
,它指向一些内存,比如 (0x012)。d
指向同一个位置(0x012)。特殊情况,按引用传递(对象)
c = {"name" : "jane"};
console.log(c); // { "name" : "jane" }
console.log(d); // { "name" : "doe" }
分享我对 JavaScript 引用的了解
在 JavaScript 中,当将对象分配给变量时,分配给变量的值是对该对象的引用:
var a = {
a: 1,
b: 2,
c: 3
};
var b = a;
// b.c is referencing to a.c value
console.log(b.c) // Output: 3
// Changing value of b.c
b.c = 4
// Also changes the value of a.c
console.log(a.c) // Output: 4
语义!!设置具体的定义必然会使一些答案和评论不兼容,因为即使使用相同的单词和短语,它们也不会描述相同的事物,但克服混淆是至关重要的(尤其是对于新程序员而言)。
首先,似乎不是每个人都掌握了多个抽象级别。学习过第 4 代或第 5 代语言的新程序员可能难以围绕汇编或 C 程序员熟悉的概念而不是指针指向指针的指针。引用传递不仅仅意味着使用函数参数变量更改引用对象的能力。
变量:符号的组合概念,它引用内存中特定位置的值。这个术语通常过于繁重,无法单独用于讨论细节。
Symbol : 用于引用变量的文本字符串(即变量的名称)。
值:存储在内存中并使用变量符号引用的特定位。
内存位置:存储变量值的位置。(位置本身由一个数字表示,与存储在该位置的值不同。)
函数参数:函数定义中声明的变量,用于引用传递给函数的变量。
函数参数:函数外部的变量,由调用者传递给函数。
对象变量:其基本基础值不是“对象”本身的变量,而是它的值是指向存储对象实际数据的内存中另一个位置的指针(内存位置值)。在大多数高代语言中,“指针”方面通过在各种上下文中的自动取消引用有效地隐藏了。
原始变量:其值为实际值的变量。即使这个概念也可以通过各种语言的自动装箱和类似对象的上下文来复杂化,但一般的想法是变量的值是由变量的符号表示的实际值,而不是指向另一个内存位置的指针。
函数实参和形参不是一回事。此外,变量的值不是变量的对象(正如很多人已经指出的那样,但显然被忽略了)。这些区别对于正确理解至关重要。
值传递或共享调用(对象):函数参数的值被复制到另一个由函数的参数符号引用的内存位置(不管它是在堆栈上还是在堆上)。换句话说,函数参数收到了传递参数值的副本......并且(关键)参数的值永远不会被调用函数更新/更改/更改。请记住,对象变量的值不是对象本身,而是指向对象的指针,因此按值传递对象变量会将指针复制到函数参数变量。函数参数的值指向内存中完全相同的对象。对象数据本身可以通过函数参数直接更改,但函数参数的值永远不会更新,因此它将继续指向相同的在整个函数调用过程中,甚至在函数调用之后(即使其对象的数据被更改或函数参数完全分配了不同的对象)。仅仅因为引用的对象可以通过函数参数变量进行更新,就得出函数参数是通过引用传递的结论是不正确的。
调用/传递引用:函数参数的值可以/将由相应的函数参数直接更新。如果有帮助,函数参数将成为参数的有效“别名”——它们有效地引用相同内存位置的相同值。如果函数参数是对象变量,则更改对象数据的能力与按值传递的情况没有什么不同,因为函数参数仍将指向与参数相同的对象。但是在对象变量的情况下,如果函数参数被设置为一个完全不同的对象,那么参数也将同样指向不同的对象——这在按值传递的情况下不会发生。
JavaScript 不通过引用传递。如果您仔细阅读,您会发现所有相反的意见都误解了按值传递的含义,并且错误地得出结论,通过函数参数更新对象数据的能力与“按值传递”同义。
对象克隆/复制:创建一个新对象并复制原始对象的数据。这可以是深拷贝或浅拷贝,但重点是创建了一个新对象。创建对象的副本是与传递值不同的概念。一些语言区分类对象和结构(或类似的),并且对于传递不同类型的变量可能有不同的行为。但是 JavaScript 在传递对象变量时不会自动执行类似的操作。但是没有自动对象克隆并不能转化为传递引用。
观察:如果观察者无法检查引擎的底层内存,则无法确定是否复制了不可变值或传递了引用。
JavaScript 或多或少与底层内存模型无关。没有参考²这样的东西。JavaScript 有值。两个变量可以保持相同的值(或更准确地说:两个环境记录可以绑定相同的值)。唯一可以改变的值类型是通过抽象 [[Get]] 和 [[Set]] 操作的对象。如果您忘记了计算机和内存,这就是描述 JavaScript 行为所需的全部内容,并且它可以让您理解规范。
let a = { prop: 1 };
let b = a; // a and b hold the same value
a.prop = "test"; // The object gets mutated, can be observed through both a and b
b = { prop: 2 }; // b holds now a different value
现在您可能会问自己,两个变量如何在计算机上保持相同的值。然后,您可能会查看 JavaScript 引擎的源代码,您很可能会发现编写引擎所用语言的程序员会调用参考的东西。
所以实际上你可以说 JavaScript 是“按值传递”,而值是可以共享的,也可以说 JavaScript 是“按引用传递”,这对于低级语言的程序员来说可能是一个有用的逻辑抽象,或者您可以将这种行为称为“通过共享调用”。
由于 JavaScript 中没有引用之类的东西,所有这些都没有错,也没有什么意义。因此,我不认为答案对搜索特别有用。
²说明书中的“参考”一词并非传统意义上的参考。它是一个对象和属性名称的容器,它是一个中间值(例如,a.b
计算结果为Reference { value = a, name = "b" }
)。术语参考有时也出现在规范中不相关的部分。
现在,人们喜欢无休止地争论“通过引用传递”是否是描述 Java 等的正确方式。实际上做。重点是:
- 传递对象不会复制对象。
- 传递给函数的对象可以使其成员被函数修改。
- 传递给函数的原始值不能被函数修改。制作了一份副本。
在我的书中,这被称为通过引用传递。
这是对此的反驳:
我理解这个的简单方法......
调用函数时,您传递的是参数变量的内容(引用或值),而不是变量本身。
var var1 = 13;
var var2 = { prop: 2 };
//13 and var2's content (reference) are being passed here
foo(var1, var2);
在函数内部,参数变量inVar1
和inVar2
接收传递的内容。
function foo(inVar1, inVar2){
//changing contents of inVar1 and inVar2 won't affect variables outside
inVar1 = 20;
inVar2 = { prop: 7 };
}
由于inVar2
收到 的引用{ prop: 2 }
,您可以更改对象属性的值。
function foo(inVar1, inVar2){
inVar2.prop = 7;
}
在 JavaScript 中向函数传递参数类似于在 C 中通过指针值传递参数:
/*
The following C program demonstrates how arguments
to JavaScript functions are passed in a way analogous
to pass-by-pointer-value in C. The original JavaScript
test case by @Shog9 follows with the translation of
the code into C. This should make things clear to
those transitioning from C to JavaScript.
function changeStuff(num, obj1, obj2)
{
num = num * 10;
obj1.item = "changed";
obj2 = {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);
This produces the output:
10
changed
unchanged
*/
#include <stdio.h>
#include <stdlib.h>
struct obj {
char *item;
};
void changeStuff(int *num, struct obj *obj1, struct obj *obj2)
{
// make pointer point to a new memory location
// holding the new integer value
int *old_num = num;
num = malloc(sizeof(int));
*num = *old_num * 10;
// make property of structure pointed to by pointer
// point to the new value
obj1->item = "changed";
// make pointer point to a new memory location
// holding the new structure value
obj2 = malloc(sizeof(struct obj));
obj2->item = "changed";
free(num); // end of scope
free(obj2); // end of scope
}
int num = 10;
struct obj obj1 = { "unchanged" };
struct obj obj2 = { "unchanged" };
int main()
{
// pass pointers by value: the pointers
// will be copied into the argument list
// of the called function and the copied
// pointers will point to the same values
// as the original pointers
changeStuff(&num, &obj1, &obj2);
printf("%d\n", num);
puts(obj1.item);
puts(obj2.item);
return 0;
}
一切都是按值传递的。
基本类型按值传递(即,将实际变量值的新副本传递给函数)。
复杂类型(对象)作为“指向对象的指针”传递。所以你传递的实际东西是一个按值传递的指针(它是一个地址,一个数值,就像其他任何东西一样)。显然,如果您尝试在函数内部修改对象的属性,即使在该函数之外,修改也会反映出来。那是因为您正在通过指向该属性的唯一副本的指针访问该属性。
这里的混淆出现在“通过值传递指针”和“通过引用传递对象”。
MDN 文档清楚地解释了它,而不是太冗长:
函数调用的参数是函数的参数。参数按值传递给函数。如果函数更改了参数的值,则此更改不会在全局或调用函数中反映出来。但是,对象引用也是值,并且它们很特殊:如果函数更改了引用对象的属性,则该更改在函数外部可见,(...)
来源:https ://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions#Description
对于编程语言律师,我已经阅读了 ECMAScript 5.1 的以下部分(比最新版本更容易阅读),并在 ECMAScript 邮件列表中询问它。
TL;DR : 一切都是按值传递的,但对象的属性是引用,并且标准中令人毛骨悚然地缺乏对象的定义。
第 11.2.4 节“参数列表”在生成仅包含 1 个参数的参数列表时说如下:
产生式 ArgumentList : AssignmentExpression 的评估如下:
- 令 ref 为评估 AssignmentExpression 的结果。
- 设 arg 为 GetValue(ref)。
- 返回一个唯一项为 arg 的列表。
该部分还列举了参数列表具有 0 或 >1 个参数的情况。
因此,一切都是通过引用传递的。
第 11.2.1 节“属性访问器”
产生式 MemberExpression : MemberExpression [ Expression ] 的评估如下:
- 让 baseReference 成为评估 MemberExpression 的结果。
- 让 baseValue 为 GetValue(baseReference)。
- 让propertyNameReference 成为计算Expression 的结果。
- 让 propertyNameValue 为 GetValue(propertyNameReference)。
- 调用 CheckObjectCoercible(baseValue)。
- 让 propertyNameString 为 ToString(propertyNameValue)。
- 如果正在评估的语法产生包含在严格模式代码中,则让 strict 为真,否则让 strict 为假。
- 返回一个 Reference 类型的值,其基值为 baseValue,其引用名称为 propertyNameString,其严格模式标志为 strict。
因此,对象的属性始终可以作为参考。
在第 8.7 节“引用规范类型”中有描述,引用不是语言中的真实类型——它们仅用于描述删除、typeof 和赋值运算符的行为。
在 5.1 版本中定义“对象是属性的集合”。因此,我们可以推断,对象的值就是集合,但是关于集合的值是什么,规范中没有定义,需要花点功夫去理解。
我发现的最简洁的解释是在AirBNB 风格指南中:
Primitives:当你访问一个基本类型时,你直接处理它的值
例如:
var foo = 1,
bar = foo;
bar = 9;
console.log(foo, bar); // => 1, 9
复杂:当您访问复杂类型时,您处理对其值的引用
例如:
var foo = [1, 2],
bar = foo;
bar[0] = 9;
console.log(foo[0], bar[0]); // => 9, 9
即有效的原始类型通过值传递,复杂类型通过引用传递。
我已经多次阅读这些答案,但直到我了解了Barbara Liskov 所称的“通过共享呼叫”的技术定义时才真正明白
共享调用的语义不同于引用调用,因为调用者看不到函数内函数参数的赋值(与引用语义不同)[需要引用],因此例如,如果传递了变量,则不可能在调用者的范围内模拟对该变量的赋值。但是,由于该函数可以访问与调用者相同的对象(不进行复制),因此如果对象是可变的,则对这些对象的突变对调用者是可见的,这可能看起来与按值调用不同语义。函数中可变对象的突变对调用者是可见的,因为该对象没有被复制或克隆——它是共享的。
也就是说,如果您访问参数值本身,参数引用是可以更改的。另一方面,对参数的赋值将在评估后消失,并且函数调用者无法访问。
如果您想要像其他语言一样的(正常)函数参数行为(传递值的副本),那么只需在传递给函数之前克隆对象:
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();
在低级语言中,如果要通过引用传递变量,则必须在创建函数时使用特定的语法:
int myAge = 14;
increaseAgeByRef(myAge);
function increaseAgeByRef(int &age) {
*age = *age + 1;
}
是对&age
的引用myAge
,但如果您想要该值,则必须转换引用,使用*age
.
JavaScript 是一种高级语言,可以为您进行这种转换。
因此,尽管对象是通过引用传递的,但语言会将引用参数转换为值。您不需要&
在函数定义上使用 , 来通过引用传递它,也不需要*
在函数体上使用 , 将引用转换为值,JavaScript 会为您完成。
这就是为什么当您尝试更改函数内的对象时,通过替换它的值(即age = {value:5}
),更改不会持续,但如果您更改它的属性(即age.value = 5
),它会持续。
我发现Underscore.js 库的extend 方法在我想将对象作为参数传递时非常有用,该参数可以被修改或完全替换。
function replaceOrModify(aObj) {
if (modify) {
aObj.setNewValue('foo');
} else {
var newObj = new MyObject();
// _.extend(destination, *sources)
_.extend(newObj, aObj);
}
}
我会说这是通过副本-
考虑参数和变量对象是在函数调用开始时创建的执行上下文期间创建的对象 - 传递给函数的实际值/引用只是存储在这个参数+变量对象中。
简单来说,对于原始类型,值在函数调用开始时被复制,对于对象类型,引用被复制。
Array 和 Object 基于这两个条件作为按引用传递或按值传递传递。
如果您使用新对象或数组更改该对象或数组的值,那么它是按值传递的。
object1 = {item: "car"};
array1=[1,2,3];
在这里,您将新对象或数组分配给旧对象。您没有更改旧对象的属性值。因此它是按值传递的。
如果您要更改对象或数组的属性值,则它是通过引用传递的。
object1.key1= "car";
array1[0]=9;
在这里,您正在更改旧对象的属性值。您没有将新对象或数组分配给旧对象。因此它是通过引用传递的。
代码
function passVar(object1, object2, number1) {
object1.key1= "laptop";
object2 = {
key2: "computer"
};
number1 = number1 + 1;
}
var object1 = {
key1: "car"
};
var object2 = {
key2: "bike"
};
var number1 = 10;
passVar(object1, object2, number1);
console.log(object1.key1);
console.log(object2.key2);
console.log(number1);
Output: -
laptop
bike
10
确定某事物是否“通过引用传递”的一种简单方法是您是否可以编写“交换”函数。例如,在 C 中,您可以执行以下操作:
void swap(int *i, int *j)
{
int t;
t = *i;
*i = *j;
*j = t;
}
如果你不能在 JavaScript 中做同样的事情,它就不是“通过引用传递”。
函数内部的简单值不会改变函数外部的值(它们通过值传递),而复杂的值会(它们通过引用传递)。
function willNotChange(x) {
x = 1;
}
var x = 1000;
willNotChange(x);
document.write('After function call, x = ' + x + '<br>'); // Still 1000
function willChange(y) {
y.num = 2;
}
var y = {num: 2000};
willChange(y);
document.write('After function call y.num = ' + y.num + '<br>'); // Now 2, not 2000
基元按值传递,对象按引用传递。这与 C、Visual Basic 或 Delphi 等其他语言完全不同。我不能确切地说它们如何处理对象和原语,但我知道 Visual Basic 和 Delphi 可以(并且应该)指定它。
PHP 从版本 5 开始做了类似的事情:所有对象都通过引用传递,但所有原语都可以通过引用传递,如果前面有一个 & 符号。否则,原语按值传递。
所以在 JavaScript 中,如果我通过参数将对象 X 传递给函数,它仍然是 X。如果您要更改函数内部的数据(或任何其他对象,但这并不重要),那么新值也可以在功能。