Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript深入之参数按值传递 #10

Open
mqyqingfeng opened this issue Apr 28, 2017 · 115 comments

Comments

@mqyqingfeng
Copy link
Owner

commented Apr 28, 2017

定义

在《JavaScript高级程序设计》第三版 4.1.3,讲到传递参数:

ECMAScript中所有函数的参数都是按值传递的。

什么是按值传递呢?

也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

按值传递

举个简单的例子:

var value = 1;
function foo(v) {
    v = 2;
    console.log(v); //2
}
foo(value);
console.log(value) // 1

很好理解,当传递 value 到函数 foo 中,相当于拷贝了一份 value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。

引用传递?

拷贝虽然很好理解,但是当值是一个复杂的数据结构的时候,拷贝就会产生性能上的问题。

所以还有另一种传递方式叫做按引用传递。

所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。

举个例子:

var obj = {
    value: 1
};
function foo(o) {
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2

哎,不对啊,连我们的红宝书都说了 ECMAScript 中所有函数的参数都是按值传递的,这怎么能按"引用传递"成功呢?

而这究竟是不是引用传递呢?

第三种传递方式

不急,让我们再看个例子:

var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1

如果 JavaScript 采用的是引用传递,外层的值也会被修改呐,这怎么又没被改呢?所以真的不是引用传递吗?

这就要讲到其实还有第三种传递方式,叫按共享传递。

而共享传递是指,在传递对象的时候,传递对象的引用的副本。

注意: 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!

所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。

最后,你可以这样理解:

参数如果是基本类型是按值传递,如果是引用类型按共享传递。

但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。

所以,高程,谁叫你是红宝书嘞!

下一篇文章

JavaScript深入之call和apply的模拟实现

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

@marty203

This comment has been minimized.

Copy link

commented May 24, 2017

我个人认为你的理解有误, 红宝书说ECMAScript中所有函数的参数都是按值传递的, 这是没错的. 关键在于如何理解值传递和引用类型, 这个概念我很早在C#上深入研究一番(在<C#本质论>的指导下). 而JavaScript的引擎是C++实现的, 所以在这一块概念上C#与C++大致一样.
C#的数据类型分为2种: 值类型和引用类型, 而方法参数的传递方式也分为2种: 值传递和引用传递, 这里要强调的是数据类型和方法参数的传递方式没有半毛钱关系. 这两者排列组合后得到4种情况: 1. 方法参数类型是值类型, 用值传递; 2. 方法参数类型是引用类型, 用值传递; 3. 方法参数类型是值类型, 用引用传递; 4. 方法参数类型是引用类型, 用引用传递.
ECMAScript如何实现方法参数用引用传递, 我实际使用中没用到过, 这里不敢妄言, 但是你在"引用传递"中举的例子, 很明显是错误的, 它只是方法参数是引用类型, 但是用的是值传递方式, 这也印证了红宝书上说的那句话.
下面我先说说C#里的这4种情况.
首先, 弄清楚方法参数传递方式. C#区分值传递和引用传递很方便, 方法参数前加ref(out修饰符这里不讨论)就是引用传递, 什么都不加就是值传递. 我们都知道方法参数有实参和形参之说, 而参数传递方式说的就是从实参给形参复制的过程. 值传递就是把实参在内存栈中的数据传递给形参, 然后你在方法内部就可以使用形参了, 而引用传递是把实参的内存栈的地址编号传递给形参.
其次, 弄清楚数据类型, 值类型就是内存中某个地址直接保存了值, 比如int i = 10;(js对应写法: var i = 10;), 运行时会在内存的栈中分配一个地址001, 并在这个地方保存10. 而引用类型则需要在内存中某个地址先保存实际的对象实例, 然后在内存的另一个地址保存指向那个对象实例的指针, 比如MyClass obj = new MyClass { value = 10 };(js对应写法: var obj = { value: 10 };), 运行时首先在内存的托管堆中保存一个MyClass的实例对象, 它的属性value=10, 再到内存的栈中分配一个地址002, 并在这里保存在托管堆中那个对象的内存地址(我们可以把这个内存地址简化理解成指向对象实例的指针). 这就是值类型和引用类型的区别.
回过来再看你的例子, 第一个是"按值传递", 这个例子符合方法参数是值类型并用值传递这种情况, value是值类型, 它在内存栈中的地址001保存了1这个数值, 在foo(value);这句, value是实参, 而foo函数声明中的v是形参, js引擎在内存栈中为形参v分配了一个地址002, 其中也保存了1这个值, 这时修改v的值, 是修改内存地址002里的值, 而地址001里的值没变, 所以在foo函数执行完, 再打印value时, 依然是1.
接下来看第二个"引用传递", 我认为这个说法是错误的, 正确的说法应该是引用类型并用值传递. obj是引用类型, 它需要在内存堆中(js引擎可能不存在托管的概念, 所以这里称为内存堆)分配一个内存地址012, 保存了它的一个对象(属性value和其值1, 这句说的不严谨, 不过不影响对本例的分析), 并在内存栈中分配了一个地址011, 这个地址保存了012(就是那个内存堆的地址, 可以理解为指针). 在foo(obj);这句, obj是实参, 而foo函数声明中的o是形参, js引擎在内存栈中为形参o分配了一个地址013, 其中也保存了012这个值, 012其实并不是像前一个例子中说的1那样的数值, 而是一个内存地址, 所以如果你打印o这个形参, 它不会把012这个值打印出来, 而是把012内存地址里保存的实例对象给打印出来. 到这里就很清楚了, 如果你修改了012指向的那个对象的属性value的值, 那么当你在打印obj这个实参时, 它的obj.value会打印出2, 而不是1.
你的第三个例子"共享传递", "共享传递"这个概念我不是很清楚, 但我觉得你举的这个例子依然是值传递, 唯一与C#不同的是, C#的变量类型定义后不能改变, 而JS的变量类型是可以随意改变的, 因此这个例子无法跟C#中的值传递来类比. 再来分析你这个例子, 首先obj实例化一个对象, 有一个属性value, 值为1, 在内存中就是现在内存堆中分配一个内存空间, 其地址为022, 保存了一个对象(包括它的属性value和值1), 然后再到内存栈中分配一个内存地址021, 保存了内存地址022这个值. 在foo(obj);这句, obj是实参, 而o是形参, 这时在内存栈中给形参o分配了一个地址023, 也保存022这个值(如果在o=2;之前打印o, 将输出undefined, 这里是由于在foo函数作用域内对变量o进行赋值操作, 因此在这个作用域内使用了局部变量o覆盖了形参o, 而局部变量o在使用时没有声明, 所以js引擎会把它的声明提升到作用域最顶部, 因此在赋值语句之前打印, 会输出undefined, 声明提升这个概念暂时也不深入展开感谢@daizengyu123 的指正, 这里因为调用foo函数时给形参o赋值了, 所以在调用o = 2;之前打印, 会输出对象{value: 1}), 而在foo函数中, 又给形参o重新赋值2, 由于2是Number类型, 这是值类型, 因此不用在内存堆中存储数据, 直接在内存栈中即可, 这句赋值语句, 相当于把内存地址023中的值022改为2, 而并没有修改内存地址021(也就是变量obj)的值, 所以在调用foo函数之后再打印obj.value时, 仍然打印出1. 这里如果把o = 2;这句替换为o = { value = 5, other = "abc" };也是同理.
最后补充一下C#中的引用类型的值传递和引用类型的引用传递的对比. 简单来说, 引用类型的值传递, 在方法内部如果对形参重新赋值, 哪怕是同一个类的对象, 在赋值后修改对象的属性, 实参的对应的属性值都不会改变, 同时实参指向的对象也不变, 而形参在重新赋值后已经指向一个新的对象了; 而引用类型的引用传递, 在方法内部如果对形参重新赋值, 那么实参也跟着重新赋值, 实参最初所指向的那个对象将不被任何变量所指向.

@mqyqingfeng

This comment has been minimized.

Copy link
Owner Author

commented May 25, 2017

哈哈,@axdhxyzx 感谢回复这么长的内容给我,我也来说下我的看法。

首先,第二个例子肯定不是真正的引用传递,这个我是知道的,毕竟我都说了ECMAScript中所有函数的参数都是按值传递的,而第二个例子就是用 JS 写的,怎么可能会是引用传递呢?我写这篇文章的思路是当值是引用类型的是时候,它可能是引用传递,因为它有着类似引用传递的表现,但是通过第三个例子,我又证明第二个例子其实不是引用传递,然后引申出第三种传递方式,按共享传递。所以虽然我写了三个例子,但是只有按值传递和按共享传递两种方式,这个在文章的最后我也讲了:“所以第二个和第三个例子其实都是按共享传递。” 不过这个地方估计让很多人都误解了,这是我的错。

其次,按共享传递依然是按值传递,我也是这样认为的呐,很多人还认为按引用传递也是按值传递,只是值是指针而已,这个说法也对,只是我们把所有的情况都归到按值传递上,看似统一了,但是如果我们要分析具体的情况时,一句按值传递可不好让人清晰的明白问题呐,所以才有了按引用传递和按共享传递的概念的出现。

最后,按共享传递的例子,如你所说, (以下可能有点不严谨,达意即可) 021 是这个对象,022是指针,023 也保存了 022 这个值,这跟文章中加粗的那一句 按共享传递是传递对象的引用的副本应该是一个意思吧,而且因为拷贝副本也是一种值的拷贝,所以你认为这也是一种值传递,这跟文章的倒数第二句 但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了应该也是一个意思吧。

欢迎讨论哈~

@marty203

This comment has been minimized.

Copy link

commented May 25, 2017

这么说吧, 不管是你前面写的文章, 还是你后面回复我的评论, 我觉得我都是能看懂的, 正如你所说的"应该也是一个意思吧".
可是如果是给初学者来看, "共享传递"这个概念该如何理解? 尤其是没有在内存堆栈这个层面说明参数传递方式的话, 初学者会不会产生误解? 我当年初学入门时, 就是因为对数据的引用类型和方法参数的引用传递没分清楚, 所以才查找书籍中的相关理论和在程序代码中进行实证的, 最终才完全搞清楚两者之间的区别.
如果只有按值传递这一种传参方式, 我们就完全没必要去讲解参数传递方式了, 只要讲清楚数据的值类型和引用类型就可以了, 毕竟值类型的值传递和引用类型的值传递在内存栈上的拷贝方式是完全相同的, 唯一差别就在于值类型和引用类型的差别了.
最后, 我说明一下, 看了你回复我的评论, 我觉得你的理解没有问题(前一条我说你理解有误, 我承认这是不对的). 只是说在JavaScript动态类型的基础上, 把值传递引申出一个"共享传递"概念, 是否会对初学者在这块理解上引起混乱, 你可以稍微考虑一下. 至此, 我对你的论述基本认同.

@mqyqingfeng

This comment has been minimized.

Copy link
Owner Author

commented May 25, 2017

感谢建议,我们俩的学习经历不一样,我也来讲讲我的学习过程。

如果是只有按值传递,作为一个没有接触栈堆的初学者,我不明白为什么在第一个例子中,原值没有被修改,而第二个例子中,原值就被修改了,难道结果不应该是原值都没有被修改吗?

于是我去查找资料,这才接触了原来还有按引用传递,所以当时的我认为当值是引用类型的时候,其实是按引用传递的。

后来看了高程,发现函数参数都是按值传递,一度开始质疑高程是写错了,直到后来接触了call by sharing 的概念,这才恍然大悟,才想明白 按值传递拷贝了原值,按共享传递拷贝了引用,都是拷贝值,所以可以理解成都是按值传递。

所以我赞同高程的说法,但到我理解高程这句话的时候,其实是经历了看山是山,看山不是山,再到看山是山的一个过程,这篇文章为什么要这么写其实就是根据我的经历而来,在我的学习过程中,理解共享传递正是我从”看山不是山“到”看山是山“的转折点。

所以还是大家的经历不一样,看待文章的角度也不一样。为了不让大家误解,我觉得应该修改一下文章。感谢你的回复,以后多多交流哈~ o( ̄▽ ̄)d

@wamich

This comment has been minimized.

Copy link

commented May 27, 2017

哈哈,本来没看懂,基于axdhxyzx的观点,觉得反而更理解mqyqingfeng的意思了。我试着说下类比的理解:

A、变量名变量值的关系好比快捷方式真实文件的关系
B、值类型类比为文件 引用类型类比为文件夹

文中的第三种传递方式
//1、2
var obj = {value: 1};
//4
function foo(o) {
//5
o = 2;
console.log(o);
}
//3
foo(obj);
console.log(obj.value)

1.创建文件夹“{value: 1}”
2.创建一个快捷方式obj
3.实参:步骤2创建的快捷方式
4.形参:创建o快捷方式,但o不指向obj指向的文件夹,却指向了快捷方式obj本身(快捷方式的快捷方式叫高阶快捷方式?哈哈,应该就是就是共享传递的意思吧)
5.修改o快捷方式的指向,改为指向文件“2”

@mqyqingfeng

This comment has been minimized.

Copy link
Owner Author

commented May 27, 2017

@wamich 形象的比喻!!!o( ̄▽ ̄)d

@sunsl516

This comment has been minimized.

Copy link

commented May 30, 2017

博主写的真好。让我之前困惑很久的问题终于得到了解答,感谢博主的无私分享。提个小意见,仅供参考,文中开头可以先普及下堆栈的概念,说明下js中普通类型和引用类型分别是以什么方式存储在内存中的,最好画个图说明,这样在接下来的讲解中会容易很多,初学者也能看得懂。

@mqyqingfeng

This comment has been minimized.

Copy link
Owner Author

commented May 31, 2017

@sunsl516 关于堆栈,我也只知道一点点……不过你启发了我,堆栈可以作为一个新课题进行研究~ o( ̄▽ ̄)d

@jawil

This comment has been minimized.

Copy link

commented May 31, 2017

计算机果然到处都是相通的,这类比我服,请收下@mqyqingfeng的膝盖。@wamich

@mqyqingfeng

This comment has been minimized.

Copy link
Owner Author

commented May 31, 2017

@jawil 哈哈,请献上自己的膝盖~😂😂😂
@wamich 不收他膝盖的话,他有1024邀请码,(๑•̀ㅂ•́)و✧

@lynn1824

This comment has been minimized.

Copy link

commented May 31, 2017

按值传递没有错
javascript中数据类型分为基本类型与引用类型;
基本类型值存储于栈内存中,传递的就是当前值,修改不会影响原有变量的值;
引用类型值其实也存于栈内存中,只是它的值是指向堆内存当中实际值的一个地址;索引引用传递传的值是栈内存当中的引用地址,当改变时,改变了堆内存当中的实际值;

@MrGoodBye

This comment has been minimized.

Copy link

commented May 31, 2017

@mqyqingfeng 我对于参数传递方式的学习路径就是:

在你这学的...

但是在了解到这个知识点之前,我大致也明白参数传递的形式.

关键点:

运算符=就是创建或修改变量在内存中的指向.
初始化变量时为创建,重新赋值即为修改.

首先一个非常简单的例子:

var a = {b: 1};// a = {b: 1}
var c = a;// c = {b: 1}
a = 2;// 重新赋值a
console.log(c);// {b: 1}

接着是上一段代码在内存中的分布:

a, c {b: 1}

然后一步一步执行代码:

  1. 创建变量a指向对象{b: 1};
  2. 创建变量c指向对象{b: 1};
  3. a重新指向常量区的2;
常量区
a 2
c {b: 1}

所以c从始至终都是指向对象{b: 1}.

var value = 1;
function foo(v) {
    v = 2;
    console.log(v); //2
}
foo(value);
console.log(value) // 1

将案例一等价替换:

var value = 1;
function foo() {
    var v = value; // 创建变量v指向value所指向的值
    v = 2;// v重新指向另外的值
    console.log(v); //2
}
foo(value);
console.log(value) // 1,value从始至终都未改变指向.

案例三也可以这样替换.

接着分析案例二:

修改一下我的第一个例子:

var a = {b: 1};// a = {b: 1}
var c = a;// c = {b: 1}
a.b = 2;// 重新赋值对象a中的属性b
console.log(c);// {b: 2},// c也随着修改,从

在内存中的分布:

常量区
a,c [[Object]]
b 1

执行完a.b = 2后:

常量区
a,c [[Object]]
b 2

那么a,c从始至终都未改变指向,只是b改变了而已
第一张内存分布图将{b: 1}放入堆中,是为了大家更方便抓住重点

所以案例二等量替换为

var obj = {
   value: 1
};
function foo() {
   var o = obj;
   o.value = 2;// 变量value改变了指向,而o并未改变
   console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
@mqyqingfeng

This comment has been minimized.

Copy link
Owner Author

commented May 31, 2017

@MrGoodBye 哈哈,感谢分享,是非常重要的补充,o( ̄▽ ̄)d

@liangtongxie

This comment has been minimized.

Copy link

commented Jun 1, 2017

@mqyqingfeng ,谢谢分享。

1、其实函数传参就是相当于给形参赋值,

第三个例子, foo(obj) 这里执行的时候, 形参部分相当于 o = obj ;
这样理解的话,和外面的赋值操作没什么区别,感觉也好理解按值传递。

2、“ 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!”

按引用传递:

   var obj = {
           age:23
      }
     var a  = obj;
     var b  = obj;

上面的a 和 b 按 @axdhxyzx 说法也是存“引用的副本”(没理解错的话),即obj实例对象的存放地址吧?

关于用共享传递这个概念,还是感觉绕了路。

欢迎讨论。

@mqyqingfeng

This comment has been minimized.

Copy link
Owner Author

commented Jun 1, 2017

@liangtongxie 仁者见仁哈~

@liangtongxie

This comment has been minimized.

Copy link

commented Jun 1, 2017

@mqyqingfeng 嗯嗯。握手。

@BuptStEve

This comment has been minimized.

Copy link

commented Jun 1, 2017

其实传递的不就是引用的值么...

@sunsl516

This comment has been minimized.

Copy link

commented Jun 1, 2017

例子一:

var value = 1;
function foo(v) {
    v = 2;
    console.log(v); //2
}
foo(value);
console.log(value) // 1

内存分布如下:

改变前:

栈内存堆内存
value1
v1
改变后:
栈内存堆内存
value1
v2

例子二:

var obj = {
value: 1
};
function foo(o) {
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2

内存分布如下:

改变前:

栈内存堆内存
obj,o 指针地址{value: 1}
改变后:
栈内存堆内存
obj,o 指针地址{value: 2}

例子三:

var obj = {
value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1

内存分布如下:

改变前:

栈内存堆内存
obj,o 指针地址{value: 1}
改变后:
栈内存堆内存
obj 指针地址{value: 1}
o 2

以上简要帮博主做个补充,这样就很明确了吧。如有不正之处欢迎指出。

@sunsl516

This comment has been minimized.

Copy link

commented Jun 1, 2017

@MrGoodBye 这个常量区这个概念有吗。我查了挺多资料都没看到呢。欢迎指点。

@mqyqingfeng

This comment has been minimized.

Copy link
Owner Author

commented Jun 2, 2017

@sunsl516 非常感谢补充~ 大家都来帮我补充,真是太感动了…… (ಥ_ಥ)

@Flying-Eagle2

This comment has been minimized.

Copy link

commented Jun 2, 2017

第三个案例较难理解,我看完大家讨论的问题才搞懂,额,我是个初学者

@a1029563229

This comment has been minimized.

Copy link

commented Jun 6, 2017

@sunsl516 @mqyqingfeng 大佬们受我一拜

@daizengyu123

This comment has been minimized.

Copy link

commented Jun 29, 2017

@axdhxyzx 讲的很明白了,不过还是有个小问题。你提到在第三个例子中如果在o=2;之前打印o, 将输出undefined。其实这里的局部变量o是有值的,不会为undefined。

@marty203

This comment has been minimized.

Copy link

commented Jun 29, 2017

@daizengyu123 你说的对, 我想错了, 而且也没有实例验证. 因为在调用foo方法时给形参o传值了, 所以在重新赋值为2之前, 是有值的, 不是undefined. 如果我原评论没有修改的话, 其他的朋友请参照这一条. 截图如下:
_20170629111113

@liubiggun

This comment has been minimized.

Copy link

commented Aug 15, 2017

有点像是c里面的指针传递呀

@cbbfcd

This comment has been minimized.

Copy link

commented Oct 25, 2017

干脆就叫拷贝传递,不管是基本数据类型还是对象类型的,都是拷贝。前者拷贝值,后者拷贝引用。

@code-play

This comment has been minimized.

Copy link

commented Jan 16, 2019

我觉得最后一个例子也是按值传递没有错

  1. o一开始的值是obj的值(按值传递),它们都是一个地址,指向obj指向的对象
  2. o赋值为2,所以o的值就是2,此时o与obj没有任何关系,obj依然指向原有的对象
@a87604476

This comment has been minimized.

Copy link

commented Feb 7, 2019

感觉将引用类型(A)的变量(*A)作为参数传递给函数,像是创建了一个指向该引用的指针(*A)的指针(**A)。然后将这个指针(**A)传递给了函数,修改(**A)的属性的操作导致A的属性值改变了,而直接赋值的话,只是改变了指针(**A)的指向,A的值并未受到影响。这么理解不知道对不对?

恩,是的,是这么理解,无论基本类型传递还是引用传递其实都是值传递,基本类型就是传递基本类型的值,比如1,2,3这样,引用类型传递的是地址值

@zhoubhin

This comment has been minimized.

Copy link

commented Feb 12, 2019

本质上都是值传递,对于基本类型,传递的是值本身,对于引用类型,传递的是引用地址。

@Chance722

This comment has been minimized.

Copy link

commented Feb 19, 2019

获益匪浅 感谢

@higboys

This comment has been minimized.

Copy link

commented Feb 27, 2019

我愣是把所有评论看完了...........................................

@fengandzhy

This comment has been minimized.

Copy link

commented Mar 5, 2019

这个插入代码怎么实现换行啊?你看我下面代码死活换行不了。
var value = 1; function foo() { var v = value; // 创建变量v指向value所指向的值 v = 2;// v重新指向另外的值 console.log(v); //2 } foo(value); console.log(value) // 1,value从始至终都未改变指向.

@xucongxin

This comment has been minimized.

Copy link

commented Mar 26, 2019

收获颇丰,感谢

@HoroXu

This comment has been minimized.

Copy link

commented Apr 1, 2019

我觉得 就是 一个 复制值 一个复制指针。看的都晕了

@DavoWU

This comment has been minimized.

Copy link

commented Apr 5, 2019

@mqyqingfeng
JavaScript中没有指针,所以在JavaScript中不存在变量指向另一个变量的引用,所有变量引用指向的都是值。函数在执行的过程中,形参对实参做一次复制,是使用值复制还是使用引用复制,则由值的类型决定。
直接举第三个例子来说明:

var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1

变量obj引用指向值 { value: 1 },foo 执行时形参对实参做一次复制使得形参o引用也是指向值 { value: 1 },这里注意o并不是指向obj;接着执行o = 2,相当于更新了o的引用,使o引用指向常量2,但是其实我们并没有改变原来的指向值 { value: 1 },所以obj的引用指向值是没有变的。如果执行的是o.value = 2,相当于是改变了原来的指向值 { value: 1 },变成了{ value: 2 },所以obj的引用指向最后的更新值 { value: 2 }。
以上从另外一个角度解释了下参数传递。

十分感谢这个回答,目前为止 最简单易懂

@chaihongjun

This comment has been minimized.

Copy link

commented May 1, 2019

函数传参,如果是基本类型变量,那么就是复制这个基本类型变量的值,如果是对象类型,复制的是这个变量的指针(内存地址)。“ECMAscript中所有函数的参数都是按值传递”。这里的值,一个是真实的变量(基本类型)值,一个是对象的内存地址。不知道这么理解对不对。

@chaihongjun

This comment has been minimized.

Copy link

commented May 1, 2019

我觉得 就是 一个 复制值 一个复制指针。看的都晕了

跟我的感觉一样,我就是这么理解的。

@rainjm

This comment has been minimized.

Copy link

commented May 8, 2019

var obj = {
    value: 1
};
function foo(o) {
    console.log(o === obj);
    o = 2;
    console.log(o); //2
}
foo(obj);

true
2

@a87604476

This comment has been minimized.

Copy link

commented May 8, 2019

@isunbeam

This comment has been minimized.

Copy link

commented May 23, 2019

//例2
var obj = {
    value: 1
};
function foo(o) {
    console.log(o === obj) //true
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
obj xxxxx(地址) {value:1}
o xxxxx(地址)
  • 当 o === obj 为true的时候就说明了传过去的不是值,而是值的存放地址
//例3
var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1
  • 当没有执行到 o=2的时候,栈和堆的存放位置还是如下这样
obj xxxxx(地址) {value:1}
o xxxxx(地址)
  • 当执行到 o=2的时候,栈和堆的存放位置就变化了.这个时候o不再存放的是地址,而是2了,所以之后打印obj还是没变化。
obj xxxxx(地址) {value:1}
o 2
@chenyin151

This comment has been minimized.

Copy link

commented May 30, 2019

哈哈,@axdhxyzx 感谢回复这么长的内容给我,我也来说下我的看法。

首先,第二个例子肯定不是真正的引用传递,这个我是知道的,毕竟我都说了ECMAScript中所有函数的参数都是按值传递的,而第二个例子就是用 JS 写的,怎么可能会是引用传递呢?我写这篇文章的思路是当值是引用类型的是时候,它可能是引用传递,因为它有着类似引用传递的表现,但是通过第三个例子,我又证明第二个例子其实不是引用传递,然后引申出第三种传递方式,按共享传递。所以虽然我写了三个例子,但是只有按值传递和按共享传递两种方式,这个在文章的最后我也讲了:“所以第二个和第三个例子其实都是按共享传递。” 不过这个地方估计让很多人都误解了,这是我的错。

其次,按共享传递依然是按值传递,我也是这样认为的呐,很多人还认为按引用传递也是按值传递,只是值是指针而已,这个说法也对,只是我们把所有的情况都归到按值传递上,看似统一了,但是如果我们要分析具体的情况时,一句按值传递可不好让人清晰的明白问题呐,所以才有了按引用传递和按共享传递的概念的出现。

最后,按共享传递的例子,如你所说, (以下可能有点不严谨,达意即可) 021 是这个对象,022是指针,023 也保存了 022 这个值,这跟文章中加粗的那一句 按共享传递是传递对象的引用的副本应该是一个意思吧,而且因为拷贝副本也是一种值的拷贝,所以你认为这也是一种值传递,这跟文章的倒数第二句 但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了应该也是一个意思吧。

欢迎讨论哈~

这里不是真正意义的引用传递,那什么是真正意义上的引用传递?能说说嘛

@chenyin151

This comment has been minimized.

Copy link

commented May 30, 2019

哈哈,@axdhxyzx 感谢回复这么长的内容给我,我也来说下我的看法。

首先,第二个例子肯定不是真正的引用传递,这个我是知道的,毕竟我都说了ECMAScript中所有函数的参数都是按值传递的,而第二个例子就是用 JS 写的,怎么可能会是引用传递呢?我写这篇文章的思路是当值是引用类型的是时候,它可能是引用传递,因为它有着类似引用传递的表现,但是通过第三个例子,我又证明第二个例子其实不是引用传递,然后引申出第三种传递方式,按共享传递。所以虽然我写了三个例子,但是只有按值传递和按共享传递两种方式,这个在文章的最后我也讲了:“所以第二个和第三个例子其实都是按共享传递。” 不过这个地方估计让很多人都误解了,这是我的错。

其次,按共享传递依然是按值传递,我也是这样认为的呐,很多人还认为按引用传递也是按值传递,只是值是指针而已,这个说法也对,只是我们把所有的情况都归到按值传递上,看似统一了,但是如果我们要分析具体的情况时,一句按值传递可不好让人清晰的明白问题呐,所以才有了按引用传递和按共享传递的概念的出现。

最后,按共享传递的例子,如你所说, (以下可能有点不严谨,达意即可) 021 是这个对象,022是指针,023 也保存了 022 这个值,这跟文章中加粗的那一句 按共享传递是传递对象的引用的副本应该是一个意思吧,而且因为拷贝副本也是一种值的拷贝,所以你认为这也是一种值传递,这跟文章的倒数第二句 但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了应该也是一个意思吧。

欢迎讨论哈~

刚查了一下资料,引用传参实际上也是按值传递,只不过这个时候的值其实就是一个内存地址指针

@chenyin151

This comment has been minimized.

Copy link

commented May 30, 2019

这么理解的
应该就是你所说的意思,也就是说js就只有一个按值传参了

@lmk15391997

This comment has been minimized.

Copy link

commented Jun 19, 2019

我愣是看完了所有的评论

@Lskkkk

This comment has been minimized.

Copy link

commented Jun 26, 2019

本来还在研究node中exports={}为什么不行,看到这篇文章豁然开朗。

@jingchaocheng

This comment has been minimized.

Copy link

commented Jul 18, 2019

红宝书 p68,下面有个注解

当复制保存着对象的某个变量时,操作的是对象的引用。但在为对象添加属性时,操作的是实际的对象。

@chiic

This comment has been minimized.

Copy link

commented Jul 26, 2019

可以理解,引用对象可发生改变。var a = {};b = a; b和a指向同一个堆内存。
但是如果b = 2;b指向a的地址就断了,指向改变了。所以才有楼主的情况三。

@llxStar

This comment has been minimized.

Copy link

commented Oct 10, 2019

我个人认为你的理解有误, 红宝书说ECMAScript中所有函数的参数都是按值传递的, 这是没错的. 关键在于如何理解值传递和引用类型, 这个概念我很早在C#上深入研究一番(在<C#本质论>的指导下). 而JavaScript的引擎是C++实现的, 所以在这一块概念上C#与C++大致一样.
C#的数据类型分为2种: 值类型和引用类型, 而方法参数的传递方式也分为2种: 值传递和引用传递, 这里要强调的是数据类型和方法参数的传递方式没有半毛钱关系. 这两者排列组合后得到4种情况: 1. 方法参数类型是值类型, 用值传递; 2. 方法参数类型是引用类型, 用值传递; 3. 方法参数类型是值类型, 用引用传递; 4. 方法参数类型是引用类型, 用引用传递.
ECMAScript如何实现方法参数用引用传递, 我实际使用中没用到过, 这里不敢妄言, 但是你在"引用传递"中举的例子, 很明显是错误的, 它只是方法参数是引用类型, 但是用的是值传递方式, 这也印证了红宝书上说的那句话.
下面我先说说C#里的这4种情况.
首先, 弄清楚方法参数传递方式. C#区分值传递和引用传递很方便, 方法参数前加ref(out修饰符这里不讨论)就是引用传递, 什么都不加就是值传递. 我们都知道方法参数有实参和形参之说, 而参数传递方式说的就是从实参给形参复制的过程. 值传递就是把实参在内存栈中的数据传递给形参, 然后你在方法内部就可以使用形参了, 而引用传递是把实参的内存栈的地址编号传递给形参.
其次, 弄清楚数据类型, 值类型就是内存中某个地址直接保存了值, 比如int i = 10;(js对应写法: var i = 10;), 运行时会在内存的栈中分配一个地址001, 并在这个地方保存10. 而引用类型则需要在内存中某个地址先保存实际的对象实例, 然后在内存的另一个地址保存指向那个对象实例的指针, 比如MyClass obj = new MyClass { value = 10 };(js对应写法: var obj = { value: 10 };), 运行时首先在内存的托管堆中保存一个MyClass的实例对象, 它的属性value=10, 再到内存的栈中分配一个地址002, 并在这里保存在托管堆中那个对象的内存地址(我们可以把这个内存地址简化理解成指向对象实例的指针). 这就是值类型和引用类型的区别.
回过来再看你的例子, 第一个是"按值传递", 这个例子符合方法参数是值类型并用值传递这种情况, value是值类型, 它在内存栈中的地址001保存了1这个数值, 在foo(value);这句, value是实参, 而foo函数声明中的v是形参, js引擎在内存栈中为形参v分配了一个地址002, 其中也保存了1这个值, 这时修改v的值, 是修改内存地址002里的值, 而地址001里的值没变, 所以在foo函数执行完, 再打印value时, 依然是1.
接下来看第二个"引用传递", 我认为这个说法是错误的, 正确的说法应该是引用类型并用值传递. obj是引用类型, 它需要在内存堆中(js引擎可能不存在托管的概念, 所以这里称为内存堆)分配一个内存地址012, 保存了它的一个对象(属性value和其值1, 这句说的不严谨, 不过不影响对本例的分析), 并在内存栈中分配了一个地址011, 这个地址保存了012(就是那个内存堆的地址, 可以理解为指针). 在foo(obj);这句, obj是实参, 而foo函数声明中的o是形参, js引擎在内存栈中为形参o分配了一个地址013, 其中也保存了012这个值, 012其实并不是像前一个例子中说的1那样的数值, 而是一个内存地址, 所以如果你打印o这个形参, 它不会把012这个值打印出来, 而是把012内存地址里保存的实例对象给打印出来. 到这里就很清楚了, 如果你修改了012指向的那个对象的属性value的值, 那么当你在打印obj这个实参时, 它的obj.value会打印出2, 而不是1.
你的第三个例子"共享传递", "共享传递"这个概念我不是很清楚, 但我觉得你举的这个例子依然是值传递, 唯一与C#不同的是, C#的变量类型定义后不能改变, 而JS的变量类型是可以随意改变的, 因此这个例子无法跟C#中的值传递来类比. 再来分析你这个例子, 首先obj实例化一个对象, 有一个属性value, 值为1, 在内存中就是现在内存堆中分配一个内存空间, 其地址为022, 保存了一个对象(包括它的属性value和值1), 然后再到内存栈中分配一个内存地址021, 保存了内存地址022这个值. 在foo(obj);这句, obj是实参, 而o是形参, 这时在内存栈中给形参o分配了一个地址023, 也保存022这个值(如果在o=2;之前打印o, 将输出undefined, 这里是由于在foo函数作用域内对变量o进行赋值操作, 因此在这个作用域内使用了局部变量o覆盖了形参o, 而局部变量o在使用时没有声明, 所以js引擎会把它的声明提升到作用域最顶部, 因此在赋值语句之前打印, 会输出undefined, 声明提升这个概念暂时也不深入展开感谢@daizengyu123 的指正, 这里因为调用foo函数时给形参o赋值了, 所以在调用o = 2;之前打印, 会输出对象{value: 1}), 而在foo函数中, 又给形参o重新赋值2, 由于2是Number类型, 这是值类型, 因此不用在内存堆中存储数据, 直接在内存栈中即可, 这句赋值语句, 相当于把内存地址023中的值022改为2, 而并没有修改内存地址021(也就是变量obj)的值, 所以在调用foo函数之后再打印obj.value时, 仍然打印出1. 这里如果把o = 2;这句替换为o = { value = 5, other = "abc" };也是同理.
最后补充一下C#中的引用类型的值传递和引用类型的引用传递的对比. 简单来说, 引用类型的值传递, 在方法内部如果对形参重新赋值, 哪怕是同一个类的对象, 在赋值后修改对象的属性, 实参的对应的属性值都不会改变, 同时实参指向的对象也不变, 而形参在重新赋值后已经指向一个新的对象了; 而引用类型的引用传递, 在方法内部如果对形参重新赋值, 那么实参也跟着重新赋值, 实参最初所指向的那个对象将不被任何变量所指向.

很好,看完了,其实就是说形参也会被分配一个新的地址,指向实参的值,恍然大悟!谢谢

@cell617

This comment has been minimized.

Copy link

commented Oct 15, 2019

这么说吧, 不管是你前面写的文章, 还是你后面回复我的评论, 我觉得我都是能看懂的, 正如你所说的"应该也是一个意思吧".
可是如果是给初学者来看, "共享传递"这个概念该如何理解? 尤其是没有在内存堆栈这个层面说明参数传递方式的话, 初学者会不会产生误解? 我当年初学入门时, 就是因为对数据的引用类型和方法参数的引用传递没分清楚, 所以才查找书籍中的相关理论和在程序代码中进行实证的, 最终才完全搞清楚两者之间的区别.
如果只有按值传递这一种传参方式, 我们就完全没必要去讲解参数传递方式了, 只要讲清楚数据的值类型和引用类型就可以了, 毕竟值类型的值传递和引用类型的值传递在内存栈上的拷贝方式是完全相同的, 唯一差别就在于值类型和引用类型的差别了.
最后, 我说明一下, 看了你回复我的评论, 我觉得你的理解没有问题(前一条我说你理解有误, 我承认这是不对的). 只是说在JavaScript动态类型的基础上, 把值传递引申出一个"共享传递"概念, 是否会对初学者在这块理解上引起混乱, 你可以稍微考虑一下. 至此, 我对你的论述基本认同.

这人就是故意装逼的,话说的不清不楚

@cell617

This comment has been minimized.

Copy link

commented Oct 15, 2019

本来还在研究node中exports={}为什么不行,看到这篇文章豁然开朗。

狗屁不看评论你能明白算我输

@cell617

This comment has been minimized.

Copy link

commented Oct 15, 2019

@liangtongxie 仁者见仁哈~
你见鬼不是鬼去吧

@cell617

This comment has been minimized.

Copy link

commented Oct 15, 2019

我个人认为你的理解有误, 红宝书说ECMAScript中所有函数的参数都是按值传递的, 这是没错的. 关键在于如何理解值传递和引用类型, 这个概念我很早在C#上深入研究一番(在<C#本质论>的指导下). 而JavaScript的引擎是C++实现的, 所以在这一块概念上C#与C++大致一样.
C#的数据类型分为2种: 值类型和引用类型, 而方法参数的传递方式也分为2种: 值传递和引用传递, 这里要强调的是数据类型和方法参数的传递方式没有半毛钱关系. 这两者排列组合后得到4种情况: 1. 方法参数类型是值类型, 用值传递; 2. 方法参数类型是引用类型, 用值传递; 3. 方法参数类型是值类型, 用引用传递; 4. 方法参数类型是引用类型, 用引用传递.
ECMAScript如何实现方法参数用引用传递, 我实际使用中没用到过, 这里不敢妄言, 但是你在"引用传递"中举的例子, 很明显是错误的, 它只是方法参数是引用类型, 但是用的是值传递方式, 这也印证了红宝书上说的那句话.
下面我先说说C#里的这4种情况.
首先, 弄清楚方法参数传递方式. C#区分值传递和引用传递很方便, 方法参数前加ref(out修饰符这里不讨论)就是引用传递, 什么都不加就是值传递. 我们都知道方法参数有实参和形参之说, 而参数传递方式说的就是从实参给形参复制的过程. 值传递就是把实参在内存栈中的数据传递给形参, 然后你在方法内部就可以使用形参了, 而引用传递是把实参的内存栈的地址编号传递给形参.
其次, 弄清楚数据类型, 值类型就是内存中某个地址直接保存了值, 比如int i = 10;(js对应写法: var i = 10;), 运行时会在内存的栈中分配一个地址001, 并在这个地方保存10. 而引用类型则需要在内存中某个地址先保存实际的对象实例, 然后在内存的另一个地址保存指向那个对象实例的指针, 比如MyClass obj = new MyClass { value = 10 };(js对应写法: var obj = { value: 10 };), 运行时首先在内存的托管堆中保存一个MyClass的实例对象, 它的属性value=10, 再到内存的栈中分配一个地址002, 并在这里保存在托管堆中那个对象的内存地址(我们可以把这个内存地址简化理解成指向对象实例的指针). 这就是值类型和引用类型的区别.
回过来再看你的例子, 第一个是"按值传递", 这个例子符合方法参数是值类型并用值传递这种情况, value是值类型, 它在内存栈中的地址001保存了1这个数值, 在foo(value);这句, value是实参, 而foo函数声明中的v是形参, js引擎在内存栈中为形参v分配了一个地址002, 其中也保存了1这个值, 这时修改v的值, 是修改内存地址002里的值, 而地址001里的值没变, 所以在foo函数执行完, 再打印value时, 依然是1.
接下来看第二个"引用传递", 我认为这个说法是错误的, 正确的说法应该是引用类型并用值传递. obj是引用类型, 它需要在内存堆中(js引擎可能不存在托管的概念, 所以这里称为内存堆)分配一个内存地址012, 保存了它的一个对象(属性value和其值1, 这句说的不严谨, 不过不影响对本例的分析), 并在内存栈中分配了一个地址011, 这个地址保存了012(就是那个内存堆的地址, 可以理解为指针). 在foo(obj);这句, obj是实参, 而foo函数声明中的o是形参, js引擎在内存栈中为形参o分配了一个地址013, 其中也保存了012这个值, 012其实并不是像前一个例子中说的1那样的数值, 而是一个内存地址, 所以如果你打印o这个形参, 它不会把012这个值打印出来, 而是把012内存地址里保存的实例对象给打印出来. 到这里就很清楚了, 如果你修改了012指向的那个对象的属性value的值, 那么当你在打印obj这个实参时, 它的obj.value会打印出2, 而不是1.
你的第三个例子"共享传递", "共享传递"这个概念我不是很清楚, 但我觉得你举的这个例子依然是值传递, 唯一与C#不同的是, C#的变量类型定义后不能改变, 而JS的变量类型是可以随意改变的, 因此这个例子无法跟C#中的值传递来类比. 再来分析你这个例子, 首先obj实例化一个对象, 有一个属性value, 值为1, 在内存中就是现在内存堆中分配一个内存空间, 其地址为022, 保存了一个对象(包括它的属性value和值1), 然后再到内存栈中分配一个内存地址021, 保存了内存地址022这个值. 在foo(obj);这句, obj是实参, 而o是形参, 这时在内存栈中给形参o分配了一个地址023, 也保存022这个值(如果在o=2;之前打印o, 将输出undefined, 这里是由于在foo函数作用域内对变量o进行赋值操作, 因此在这个作用域内使用了局部变量o覆盖了形参o, 而局部变量o在使用时没有声明, 所以js引擎会把它的声明提升到作用域最顶部, 因此在赋值语句之前打印, 会输出undefined, 声明提升这个概念暂时也不深入展开感谢@daizengyu123 的指正, 这里因为调用foo函数时给形参o赋值了, 所以在调用o = 2;之前打印, 会输出对象{value: 1}), 而在foo函数中, 又给形参o重新赋值2, 由于2是Number类型, 这是值类型, 因此不用在内存堆中存储数据, 直接在内存栈中即可, 这句赋值语句, 相当于把内存地址023中的值022改为2, 而并没有修改内存地址021(也就是变量obj)的值, 所以在调用foo函数之后再打印obj.value时, 仍然打印出1. 这里如果把o = 2;这句替换为o = { value = 5, other = "abc" };也是同理.
最后补充一下C#中的引用类型的值传递和引用类型的引用传递的对比. 简单来说, 引用类型的值传递, 在方法内部如果对形参重新赋值, 哪怕是同一个类的对象, 在赋值后修改对象的属性, 实参的对应的属性值都不会改变, 同时实参指向的对象也不变, 而形参在重新赋值后已经指向一个新的对象了; 而引用类型的引用传递, 在方法内部如果对形参重新赋值, 那么实参也跟着重新赋值, 实参最初所指向的那个对象将不被任何变量所指向.

而并没有修改内存地址021(也就是变量obj)的值 你搞错了吧,021是栈内存地址改变它干嘛,应该是没有改变021这个指针指向的堆内存的对象的值1吧 老哥还是画个图吧,你把我都搞懵逼了 你们怎么都不喜欢画图呢

@luxiaojie322

This comment has been minimized.

Copy link

commented Oct 15, 2019

浅拷贝?

@cell617

This comment has been minimized.

Copy link

commented Oct 16, 2019

@mqyqingfeng
JavaScript中没有指针,所以在JavaScript中不存在变量指向另一个变量的引用,所有变量引用指向的都是值。函数在执行的过程中,形参对实参做一次复制,是使用值复制还是使用引用复制,则由值的类型决定。
直接举第三个例子来说明:

var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1

变量obj引用指向值 { value: 1 },foo 执行时形参对实参做一次复制使得形参o引用也是指向值 { value: 1 },这里注意o并不是指向obj;接着执行o = 2,相当于更新了o的引用,使o引用指向常量2,但是其实我们并没有改变原来的指向值 { value: 1 },所以obj的引用指向值是没有变的。如果执行的是o.value = 2,相当于是改变了原来的指向值 { value: 1 },变成了{ value: 2 },所以obj的引用指向最后的更新值 { value: 2 }。
以上从另外一个角度解释了下参数传递。

为什么o里保存的指针地址却可以打印堆内存中对象的内容 不是应该打印出来堆内存这个对象的地址 吗

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.