-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Comments
我个人认为你的理解有误, 红宝书说 ECMAScript 中所有函数的参数都是按值传递的, 这是没错的. 关键在于如何理解值传递和引用类型, 这个概念我很早在C#上深入研究一番(在<C#本质论>的指导下). 而 JavaScript 的引擎是 C++ 实现的, 所以在这一块概念上C# 与 C++ 大致一样. C# 的数据类型分为 2 种: 值类型和引用类型, 而方法参数的传递方式也分为 2 种: 值传递和引用传递, 这里要强调的是数据类型和方法参数的传递方式没有半毛钱关系. 这两者排列组合后得到4种情况:
ECMAScript 如何实现方法参数用引用传递, 我实际使用中没用到过, 这里不敢妄言, 但是你在"引用传递"中举的例子, 很明显是错误的, 它只是方法参数是引用类型, 但是用的是值传递方式, 这也印证了红宝书上说的那句话. 下面我先说说 C# 里的这4种情况. 首先, 弄清楚方法参数传递方式. C# 区分值传递和引用传递很方便, 方法参数前加ref (out修饰符这里不讨论)就是引用传递, 什么都不加就是值传递. 我们都知道方法参数有实参和形参之说, 而参数传递方式说的就是从实参给形参复制的过程. 值传递就是把实参在内存栈中的数据传递给形参, 然后你在方法内部就可以使用形参了, 而引用传递是把实参的内存栈的地址编号传递给形参. 其次, 弄清楚数据类型, 值类型就是内存中某个地址直接保存了值, 比如 回过来再看你的例子, 第一个是"按值传递", 这个例子符合方法参数是值类型并用值传递这种情况, 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这个值( 最后补充一下C#中的引用类型的值传递和引用类型的引用传递的对比. 简单来说, 引用类型的值传递, 在方法内部如果对形参重新赋值, 哪怕是同一个类的对象, 在赋值后修改对象的属性, 实参的对应的属性值都不会改变, 同时实参指向的对象也不变, 而形参在重新赋值后已经指向一个新的对象了; 而引用类型的引用传递, 在方法内部如果对形参重新赋值, 那么实参也跟着重新赋值, 实参最初所指向的那个对象将不被任何变量所指向. |
哈哈,@axdhxyzx 感谢回复这么长的内容给我,我也来说下我的看法。 首先,第二个例子肯定不是真正的引用传递,这个我是知道的,毕竟我都说了ECMAScript中所有函数的参数都是按值传递的,而第二个例子就是用 JS 写的,怎么可能会是引用传递呢?我写这篇文章的思路是当值是引用类型的是时候,它可能是引用传递,因为它有着类似引用传递的表现,但是通过第三个例子,我又证明第二个例子其实不是引用传递,然后引申出第三种传递方式,按共享传递。所以虽然我写了三个例子,但是只有按值传递和按共享传递两种方式,这个在文章的最后我也讲了:“所以第二个和第三个例子其实都是按共享传递。” 不过这个地方估计让很多人都误解了,这是我的错。 其次,按共享传递依然是按值传递,我也是这样认为的呐,很多人还认为按引用传递也是按值传递,只是值是指针而已,这个说法也对,只是我们把所有的情况都归到按值传递上,看似统一了,但是如果我们要分析具体的情况时,一句按值传递可不好让人清晰的明白问题呐,所以才有了按引用传递和按共享传递的概念的出现。 最后,按共享传递的例子,如你所说, (以下可能有点不严谨,达意即可) 021 是这个对象,022是指针,023 也保存了 022 这个值,这跟文章中加粗的那一句 按共享传递是传递对象的引用的副本应该是一个意思吧,而且因为拷贝副本也是一种值的拷贝,所以你认为这也是一种值传递,这跟文章的倒数第二句 但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了应该也是一个意思吧。 欢迎讨论哈~ |
这么说吧, 不管是你前面写的文章, 还是你后面回复我的评论, 我觉得我都是能看懂的, 正如你所说的"应该也是一个意思吧". 可是如果是给初学者来看, "共享传递"这个概念该如何理解? 尤其是没有在内存堆栈这个层面说明参数传递方式的话, 初学者会不会产生误解? 我当年初学入门时, 就是因为对数据的引用类型和方法参数的引用传递没分清楚, 所以才查找书籍中的相关理论和在程序代码中进行实证的, 最终才完全搞清楚两者之间的区别. 如果只有按值传递这一种传参方式, 我们就完全没必要去讲解参数传递方式了, 只要讲清楚数据的值类型和引用类型就可以了, 毕竟值类型的值传递和引用类型的值传递在内存栈上的拷贝方式是完全相同的, 唯一差别就在于值类型和引用类型的差别了. 最后, 我说明一下, 看了你回复我的评论, 我觉得你的理解没有问题(前一条我说你理解有误, 我承认这是不对的). 只是说在JavaScript动态类型的基础上, 把值传递引申出一个"共享传递"概念, 是否会对初学者在这块理解上引起混乱, 你可以稍微考虑一下. 至此, 我对你的论述基本认同. |
感谢建议,我们俩的学习经历不一样,我也来讲讲我的学习过程。 如果是只有按值传递,作为一个没有接触栈堆的初学者,我不明白为什么在第一个例子中,原值没有被修改,而第二个例子中,原值就被修改了,难道结果不应该是原值都没有被修改吗? 于是我去查找资料,这才接触了原来还有按引用传递,所以当时的我认为当值是引用类型的时候,其实是按引用传递的。 后来看了高程,发现函数参数都是按值传递,一度开始质疑高程是写错了,直到后来接触了call by sharing 的概念,这才恍然大悟,才想明白 按值传递拷贝了原值,按共享传递拷贝了引用,都是拷贝值,所以可以理解成都是按值传递。 所以我赞同高程的说法,但到我理解高程这句话的时候,其实是经历了看山是山,看山不是山,再到看山是山的一个过程,这篇文章为什么要这么写其实就是根据我的经历而来,在我的学习过程中,理解共享传递正是我从”看山不是山“到”看山是山“的转折点。 所以还是大家的经历不一样,看待文章的角度也不一样。为了不让大家误解,我觉得应该修改一下文章。感谢你的回复,以后多多交流哈~ o( ̄▽ ̄)d |
哈哈,本来没看懂,基于axdhxyzx的观点,觉得反而更理解mqyqingfeng的意思了。我试着说下类比的理解: A、变量名与变量值的关系好比快捷方式与真实文件的关系 文中的第三种传递方式
//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}” |
@wamich 形象的比喻!!!o( ̄▽ ̄)d |
博主写的真好。让我之前困惑很久的问题终于得到了解答,感谢博主的无私分享。提个小意见,仅供参考,文中开头可以先普及下堆栈的概念,说明下js中普通类型和引用类型分别是以什么方式存储在内存中的,最好画个图说明,这样在接下来的讲解中会容易很多,初学者也能看得懂。 |
@sunsl516 关于堆栈,我也只知道一点点……不过你启发了我,堆栈可以作为一个新课题进行研究~ o( ̄▽ ̄)d |
计算机果然到处都是相通的,这类比我服,请收下@mqyqingfeng的膝盖。@wamich |
按值传递没有错 |
@mqyqingfeng 我对于参数传递方式的学习路径就是:
但是在了解到这个知识点之前,我大致也明白参数传递的形式. 关键点:
首先一个非常简单的例子:
接着是上一段代码在内存中的分布:
然后一步一步执行代码:
将案例一等价替换:
案例三也可以这样替换. 接着分析案例二: 修改一下我的第一个例子:
在内存中的分布:
执行完
所以案例二等量替换为
|
@MrGoodBye 哈哈,感谢分享,是非常重要的补充,o( ̄▽ ̄)d |
@mqyqingfeng ,谢谢分享。 1、其实函数传参就是相当于给形参赋值, 第三个例子, foo(obj) 这里执行的时候, 形参部分相当于 o = obj ; 2、“ 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!” 按引用传递:
上面的a 和 b 按 @axdhxyzx 说法也是存“引用的副本”(没理解错的话),即obj实例对象的存放地址吧? 关于用共享传递这个概念,还是感觉绕了路。 欢迎讨论。 |
@liangtongxie 仁者见仁哈~ |
@mqyqingfeng 嗯嗯。握手。 |
其实传递的不就是引用的值么... |
例子一:
内存分布如下: 改变前:
例子二:
内存分布如下: 改变前:
例子三:
内存分布如下: 改变前:
以上简要帮博主做个补充,这样就很明确了吧。如有不正之处欢迎指出。 |
@MrGoodBye 这个常量区这个概念有吗。我查了挺多资料都没看到呢。欢迎指点。 |
@sunsl516 非常感谢补充~ 大家都来帮我补充,真是太感动了…… (ಥ_ಥ) |
第三个案例较难理解,我看完大家讨论的问题才搞懂,额,我是个初学者 |
@sunsl516 @mqyqingfeng 大佬们受我一拜 |
@axdhxyzx 讲的很明白了,不过还是有个小问题。你提到在第三个例子中如果在o=2;之前打印o, 将输出undefined。其实这里的局部变量o是有值的,不会为undefined。 |
@daizengyu123 你说的对, 我想错了, 而且也没有实例验证. 因为在调用foo方法时给形参o传值了, 所以在重新赋值为2之前, 是有值的, 不是undefined. 如果我原评论没有修改的话, 其他的朋友请参照这一条. 截图如下: |
有点像是c里面的指针传递呀 |
干脆就叫拷贝传递,不管是基本数据类型还是对象类型的,都是拷贝。前者拷贝值,后者拷贝引用。 |
当形参对应的实参是对象时,形参的值 = 指向对象的指针,在函数内部对形参赋值,被抹去的是指针的值,不影响指针所指的对象。 |
基本类型和引用类型变量复制过程应该是这样来解释的吧 |
引用传递是否是传入该变量的地址,而不是传入该变量指向的地址呢? 如果是的话好像js中没有这种方式吧? 请大神们赐教,感激不敬。 |
高程说值传递没错吧。 let o = { a : 1};
function f(obj){
// o 传递给obj,o和obj指向同一块地址
console.log(o === obj); // true
obj.a = 2;
obj = null;
}
f(o);
console.log(o.a); // 2``` |
函数执行时会创建活动对象,词法分析时函数形参一样会有变量提升,被定义为 var obj = {
value: 1
};
function foo(a,b) {
console.log(a.value); //1
a.value = 2
b=2
}
foo(obj,obj);
console.log(obj.value) // 2
函数自始自终也都是拷贝传递。 |
是的感觉用C语言的指针更好理解些 |
抛开函数传参数这个话题 先看看一个简单的栗子
首先有以下步骤
然后在执行代码时
我感觉噢 函数传递方式和以上的形式本质上都是一样的。 原文以下代码
在给foo函数传递obj参数的时候,此时函数参数o和obj对象指向了同一个堆内存,但是在执行foo的代码阶段时,参数o指向发生变化了。导致参数o指向了栈内存2,而obj对象还是指向堆内存{value: 1}变量。 这里我个人认为,更多的是变量在内存中的变化,跟是否是函数参数没有关系啦。都是按值传递吧嘻嘻。😝 |
首先我觉得这篇文章确实让我有了新的认知,“共享传递”,这个我是不认可的将简单的道理讲复杂的。从内存的角度去看,JavaScript中的数据类型无非两种,原始类型和引用类型。前者存放在栈内存中,后者存放在堆内存中,两者区别我就不详细说了,这个区别就决定了引用类型的赋值,赋的也是引用类型存放在栈内存中的地址,故也是按值传递,所以只有按值传递这一说,“共享传递”理解上又搞复杂了。详见字节跳动大佬的总结 |
以前也不怎么懂,学了c++瞬间就懂了,什么Java JavaScript 中说的引用就是c++的指针,跟c++中所说引用完全不同,c++中的引用只是一个对象的别名,声明时必须绑定一个存在对象,不能解绑。 |
这不就C语言的指针嘛,我感觉只要指针理解到位了,什么引用传递,值传递都是弟弟。 |
|
评论区的各位太强了,真的学习到很多 |
数据类型分为两种 值类型,引用类型 。但是参数传递 和数据类型没关系,不管是什么数据类型,都是值传递。 你传一个对象,他是引用类型参数,但是从实参到形参的过程,传递方式是值传递,不涉及任何引用,是两个东西。(若非如此,形参赋值一个新对象,原来对象也得变) |
就相当于参数赋值给一个变量?
` |
这是来自QQ邮箱的假期自动回复邮件。
您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
这是来自QQ邮箱的假期自动回复邮件。您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
不是呀,要是传递的是引用的值,第三个例子指针就被修改了,就无法指回原来的对象了 |
这是来自QQ邮箱的假期自动回复邮件。您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
所以说,纠结到底是按什么方式传递,没啥用。就记住变量之间的赋值,是浅拷贝,至于浅拷贝原理,这个应该都明白! |
这是来自QQ邮箱的假期自动回复邮件。您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
这是来自QQ邮箱的假期自动回复邮件。
您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
这是来自QQ邮箱的假期自动回复邮件。您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
1 similar comment
这是来自QQ邮箱的假期自动回复邮件。您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
这是来自QQ邮箱的假期自动回复邮件。
您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
这是来自QQ邮箱的假期自动回复邮件。你好,你的邮件我收到,谢谢。
|
定义
在《JavaScript高级程序设计》第三版 4.1.3,讲到传递参数:
什么是按值传递呢?
按值传递
举个简单的例子:
很好理解,当传递 value 到函数 foo 中,相当于拷贝了一份 value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。
引用传递?
拷贝虽然很好理解,但是当值是一个复杂的数据结构的时候,拷贝就会产生性能上的问题。
所以还有另一种传递方式叫做按引用传递。
所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。
举个例子:
哎,不对啊,连我们的红宝书都说了 ECMAScript 中所有函数的参数都是按值传递的,这怎么能按"引用传递"成功呢?
而这究竟是不是引用传递呢?
第三种传递方式
不急,让我们再看个例子:
如果 JavaScript 采用的是引用传递,外层的值也会被修改呐,这怎么又没被改呢?所以真的不是引用传递吗?
这就要讲到其实还有第三种传递方式,叫按共享传递。
而共享传递是指,在传递对象的时候,传递对象的引用的副本。
注意: 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!
所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。
最后,你可以这样理解:
参数如果是基本类型是按值传递,如果是引用类型按共享传递。
但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。
所以,高程,谁叫你是红宝书嘞!
下一篇文章
JavaScript深入之call和apply的模拟实现
深入系列
JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: