Blog
Blog copied to clipboard
JavaScript深入之参数按值传递
定义
在《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深入系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。
我个人认为你的理解有误, 红宝书说 ECMAScript 中所有函数的参数都是按值传递的, 这是没错的. 关键在于如何理解值传递和引用类型, 这个概念我很早在C#上深入研究一番(在<C#本质论>的指导下). 而 JavaScript 的引擎是 C++ 实现的, 所以在这一块概念上C# 与 C++ 大致一样.
C# 的数据类型分为 2 种: 值类型和引用类型, 而方法参数的传递方式也分为 2 种: 值传递和引用传递, 这里要强调的是数据类型和方法参数的传递方式没有半毛钱关系. 这两者排列组合后得到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#中的引用类型的值传递和引用类型的引用传递的对比. 简单来说, 引用类型的值传递, 在方法内部如果对形参重新赋值, 哪怕是同一个类的对象, 在赋值后修改对象的属性, 实参的对应的属性值都不会改变, 同时实参指向的对象也不变, 而形参在重新赋值后已经指向一个新的对象了; 而引用类型的引用传递, 在方法内部如果对形参重新赋值, 那么实参也跟着重新赋值, 实参最初所指向的那个对象将不被任何变量所指向.
哈哈,@axdhxyzx 感谢回复这么长的内容给我,我也来说下我的看法。
首先,第二个例子肯定不是真正的引用传递,这个我是知道的,毕竟我都说了ECMAScript中所有函数的参数都是按值传递的,而第二个例子就是用 JS 写的,怎么可能会是引用传递呢?我写这篇文章的思路是当值是引用类型的是时候,它可能是引用传递,因为它有着类似引用传递的表现,但是通过第三个例子,我又证明第二个例子其实不是引用传递,然后引申出第三种传递方式,按共享传递。所以虽然我写了三个例子,但是只有按值传递和按共享传递两种方式,这个在文章的最后我也讲了:“所以第二个和第三个例子其实都是按共享传递。” 不过这个地方估计让很多人都误解了,这是我的错。
其次,按共享传递依然是按值传递,我也是这样认为的呐,很多人还认为按引用传递也是按值传递,只是值是指针而已,这个说法也对,只是我们把所有的情况都归到按值传递上,看似统一了,但是如果我们要分析具体的情况时,一句按值传递可不好让人清晰的明白问题呐,所以才有了按引用传递和按共享传递的概念的出现。
最后,按共享传递的例子,如你所说, (以下可能有点不严谨,达意即可) 021 是这个对象,022是指针,023 也保存了 022 这个值,这跟文章中加粗的那一句 按共享传递是传递对象的引用的副本应该是一个意思吧,而且因为拷贝副本也是一种值的拷贝,所以你认为这也是一种值传递,这跟文章的倒数第二句 但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了应该也是一个意思吧。
欢迎讨论哈~
这么说吧, 不管是你前面写的文章, 还是你后面回复我的评论, 我觉得我都是能看懂的, 正如你所说的"应该也是一个意思吧".
可是如果是给初学者来看, "共享传递"这个概念该如何理解? 尤其是没有在内存堆栈这个层面说明参数传递方式的话, 初学者会不会产生误解? 我当年初学入门时, 就是因为对数据的引用类型和方法参数的引用传递没分清楚, 所以才查找书籍中的相关理论和在程序代码中进行实证的, 最终才完全搞清楚两者之间的区别.
如果只有按值传递这一种传参方式, 我们就完全没必要去讲解参数传递方式了, 只要讲清楚数据的值类型和引用类型就可以了, 毕竟值类型的值传递和引用类型的值传递在内存栈上的拷贝方式是完全相同的, 唯一差别就在于值类型和引用类型的差别了.
最后, 我说明一下, 看了你回复我的评论, 我觉得你的理解没有问题(前一条我说你理解有误, 我承认这是不对的). 只是说在JavaScript动态类型的基础上, 把值传递引申出一个"共享传递"概念, 是否会对初学者在这块理解上引起混乱, 你可以稍微考虑一下. 至此, 我对你的论述基本认同.
感谢建议,我们俩的学习经历不一样,我也来讲讲我的学习过程。
如果是只有按值传递,作为一个没有接触栈堆的初学者,我不明白为什么在第一个例子中,原值没有被修改,而第二个例子中,原值就被修改了,难道结果不应该是原值都没有被修改吗?
于是我去查找资料,这才接触了原来还有按引用传递,所以当时的我认为当值是引用类型的时候,其实是按引用传递的。
后来看了高程,发现函数参数都是按值传递,一度开始质疑高程是写错了,直到后来接触了call by sharing 的概念,这才恍然大悟,才想明白 按值传递拷贝了原值,按共享传递拷贝了引用,都是拷贝值,所以可以理解成都是按值传递。
所以我赞同高程的说法,但到我理解高程这句话的时候,其实是经历了看山是山,看山不是山,再到看山是山的一个过程,这篇文章为什么要这么写其实就是根据我的经历而来,在我的学习过程中,理解共享传递正是我从”看山不是山“到”看山是山“的转折点。
所以还是大家的经历不一样,看待文章的角度也不一样。为了不让大家误解,我觉得应该修改一下文章。感谢你的回复,以后多多交流哈~ o( ̄▽ ̄)d
哈哈,本来没看懂,基于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”
@wamich 形象的比喻!!!o( ̄▽ ̄)d
博主写的真好。让我之前困惑很久的问题终于得到了解答,感谢博主的无私分享。提个小意见,仅供参考,文中开头可以先普及下堆栈的概念,说明下js中普通类型和引用类型分别是以什么方式存储在内存中的,最好画个图说明,这样在接下来的讲解中会容易很多,初学者也能看得懂。
@sunsl516 关于堆栈,我也只知道一点点……不过你启发了我,堆栈可以作为一个新课题进行研究~ o( ̄▽ ̄)d
计算机果然到处都是相通的,这类比我服,请收下@mqyqingfeng的膝盖。@wamich
@jawil 哈哈,请献上自己的膝盖~😂😂😂 @wamich 不收他膝盖的话,他有1024邀请码,(๑•̀ㅂ•́)و✧
按值传递没有错 javascript中数据类型分为基本类型与引用类型; 基本类型值存储于栈内存中,传递的就是当前值,修改不会影响原有变量的值; 引用类型值其实也存于栈内存中,只是它的值是指向堆内存当中实际值的一个地址;索引引用传递传的值是栈内存当中的引用地址,当改变时,改变了堆内存当中的实际值;
@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} |
然后一步一步执行代码:
- 创建变量a指向
对象{b: 1}; - 创建变量c指向
对象{b: 1}; - 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
@MrGoodBye 哈哈,感谢分享,是非常重要的补充,o( ̄▽ ̄)d
@mqyqingfeng ,谢谢分享。
1、其实函数传参就是相当于给形参赋值,
第三个例子, foo(obj) 这里执行的时候, 形参部分相当于 o = obj ; 这样理解的话,和外面的赋值操作没什么区别,感觉也好理解按值传递。
2、“ 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!”
按引用传递:
var obj = {
age:23
}
var a = obj;
var b = obj;
上面的a 和 b 按 @axdhxyzx 说法也是存“引用的副本”(没理解错的话),即obj实例对象的存放地址吧?
关于用共享传递这个概念,还是感觉绕了路。
欢迎讨论。
@liangtongxie 仁者见仁哈~
@mqyqingfeng 嗯嗯。握手。
其实传递的不就是引用的值么...
例子一:
var value = 1;
function foo(v) {
v = 2;
console.log(v); //2
}
foo(value);
console.log(value) // 1
内存分布如下:
改变前:
| 栈内存 | 堆内存 | ||
| value | 1 | ||
| v | 1 | ||
| 栈内存 | 堆内存 | ||
| value | 1 | ||
| v | 2 | ||
例子二:
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 | |
以上简要帮博主做个补充,这样就很明确了吧。如有不正之处欢迎指出。
@MrGoodBye 这个常量区这个概念有吗。我查了挺多资料都没看到呢。欢迎指点。
@sunsl516 非常感谢补充~ 大家都来帮我补充,真是太感动了…… (ಥ_ಥ)
第三个案例较难理解,我看完大家讨论的问题才搞懂,额,我是个初学者
@sunsl516 @mqyqingfeng 大佬们受我一拜
@axdhxyzx 讲的很明白了,不过还是有个小问题。你提到在第三个例子中如果在o=2;之前打印o, 将输出undefined。其实这里的局部变量o是有值的,不会为undefined。
@daizengyu123 你说的对, 我想错了, 而且也没有实例验证. 因为在调用foo方法时给形参o传值了, 所以在重新赋值为2之前, 是有值的, 不是undefined. 如果我原评论没有修改的话, 其他的朋友请参照这一条. 截图如下:

有点像是c里面的指针传递呀
干脆就叫拷贝传递,不管是基本数据类型还是对象类型的,都是拷贝。前者拷贝值,后者拷贝引用。
@cbbfcd 哈哈~ 你道出了本质,就是这样
ECMAScript中规定,所有函数的参数都是按值传递的,我也觉得第二个例子不应该给一个##引用传递 的标题,这样会让新手产生概念的误解,当然,您在末尾提到了本质是按值传递的。但是我还是觉得第二个例子的标题容易带偏新手的理解。
@ppmiao0628 说的非常在理~ o( ̄▽ ̄)d
@mqyqingfeng
// 第一种情况
var aa = {value: 1};
var bb = aa;
bb = 2;
console.log(aa); // {value: 1};
// 第二种情况
var aa = {value: 1};
var bb = aa;
bb.value = 2;
console.log(aa); // {value: 2};
第二和第三个例子跟我上面写的不是一样吗? 我上面写的就是引用复制啊,求解答。