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 · 151 comments
Open

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

mqyqingfeng opened this issue Apr 28, 2017 · 151 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng 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
Copy link

marty203 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
Copy link
Owner Author

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

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

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

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

欢迎讨论哈~

@marty203
Copy link

marty203 commented May 25, 2017

这么说吧, 不管是你前面写的文章, 还是你后面回复我的评论, 我觉得我都是能看懂的, 正如你所说的"应该也是一个意思吧".

可是如果是给初学者来看, "共享传递"这个概念该如何理解? 尤其是没有在内存堆栈这个层面说明参数传递方式的话, 初学者会不会产生误解? 我当年初学入门时, 就是因为对数据的引用类型和方法参数的引用传递没分清楚, 所以才查找书籍中的相关理论和在程序代码中进行实证的, 最终才完全搞清楚两者之间的区别.

如果只有按值传递这一种传参方式, 我们就完全没必要去讲解参数传递方式了, 只要讲清楚数据的值类型和引用类型就可以了, 毕竟值类型的值传递和引用类型的值传递在内存栈上的拷贝方式是完全相同的, 唯一差别就在于值类型和引用类型的差别了.

最后, 我说明一下, 看了你回复我的评论, 我觉得你的理解没有问题(前一条我说你理解有误, 我承认这是不对的). 只是说在JavaScript动态类型的基础上, 把值传递引申出一个"共享传递"概念, 是否会对初学者在这块理解上引起混乱, 你可以稍微考虑一下. 至此, 我对你的论述基本认同.

@mqyqingfeng
Copy link
Owner Author

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

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

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

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

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

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

@wamich
Copy link

wamich 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
Copy link
Owner Author

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

@sunsl516
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@jawil
Copy link

jawil commented May 31, 2017

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

@mqyqingfeng
Copy link
Owner Author

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

@lynn1824
Copy link

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

@MrGoodBye
Copy link

MrGoodBye 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
Copy link
Owner Author

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

@liangtongxie
Copy link

liangtongxie 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
Copy link
Owner Author

@liangtongxie 仁者见仁哈~

@liangtongxie
Copy link

@mqyqingfeng 嗯嗯。握手。

@BuptStEve
Copy link

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

@sunsl516
Copy link

sunsl516 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
Copy link

sunsl516 commented Jun 1, 2017

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

@mqyqingfeng
Copy link
Owner Author

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

@Flying-Eagle2
Copy link

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

@a1029563229
Copy link

@sunsl516 @mqyqingfeng 大佬们受我一拜

@daizengyu123
Copy link

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

@marty203
Copy link

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

@liubiggun
Copy link

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

@cbbfcd
Copy link

cbbfcd commented Oct 25, 2017

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

@whxcode
Copy link

whxcode commented Mar 27, 2020

无论是 JavaScript 还是 c 其本质都是按值传递,大家所说的引用传递其本质也是值,只不过这个值,是某一个对象的地址,通过这个地址可以修改这个对象中的所持有的值。
image
!! 以上内容都是参考《c和指针》

@OldDream
Copy link

当形参对应的实参是对象时,形参的值 = 指向对象的指针,在函数内部对形参赋值,被抹去的是指针的值,不影响指针所指的对象。

@shujiawei
Copy link

基本类型和引用类型变量复制过程应该是这样来解释的吧

@Dedicatus546
Copy link

引用传递是否是传入该变量的地址,而不是传入该变量指向的地址呢?

如果是的话好像js中没有这种方式吧?

请大神们赐教,感激不敬。

@yushen7
Copy link

yushen7 commented Apr 6, 2020

高程说值传递没错吧。
只不过数值类型传递的是一个实实在在的值,而引用类型传递的是一个地址。

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```

@linzhenjie
Copy link

我觉得说引用和堆栈都有点太复杂了,依我的理解:

函数执行时会创建活动对象,词法分析时函数形参一样会有变量提升,被定义为undefined,执行时会进行值拷贝。区别是:拷贝值还是拷贝地址。

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

function foo中的ab,词法分析都已经定义好了,执行时对变量进行赋值,都拷贝了obj的地址,而对a.value的修改是改了地址内value的值,并不是改a的值,而对b的修改则是改变量这个值。所以外层的obj自始自终都是指向地址,地址里的obj.value被改了。

函数自始自终也都是拷贝传递。

@cliYao
Copy link

cliYao commented May 12, 2020

例2,例3是一样的都是传递指针,区别在于对传入的指针值进行了不同的操作 ;o.value=2不会改变指针指向,而只是改变的指针指向的对象内部,而o=2会改变指针指向。

是的感觉用C语言的指针更好理解些

@KeithChou
Copy link

抛开函数传参数这个话题 先看看一个简单的栗子

var value1 = { a: 1 }
var value2 = value1

value2 = { a: 2 }
console.log(value1);
console.log(value2);

首先有以下步骤

  1. 进入全局上下文
  2. 初始化变量对象,即函数声明、变量声明等。

然后在执行代码时

  1. 给value1变量分配对内存,指向{a: 1}对象
  2. 将value1变量赋值给value2变量。这里要注意的是,value2指向的是value1变量对应的对内存,value1变量只是这个堆内存的引用罢了
  3. 将{a: 2}对象赋值给value2变量。此时,value2变量指向的堆内存发生变化,而value1没有变化,仍然是{a: 1}对象。value1和value2此时指向不同的堆内存

我感觉噢 函数传递方式和以上的形式本质上都是一样的。

原文以下代码

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

在给foo函数传递obj参数的时候,此时函数参数o和obj对象指向了同一个堆内存,但是在执行foo的代码阶段时,参数o指向发生变化了。导致参数o指向了栈内存2,而obj对象还是指向堆内存{value: 1}变量。

这里我个人认为,更多的是变量在内存中的变化,跟是否是函数参数没有关系啦。都是按值传递吧嘻嘻。😝

@HduSy
Copy link

HduSy commented Aug 23, 2020

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

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

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

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

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

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

首先我觉得这篇文章确实让我有了新的认知,“共享传递”,这个我是不认可的将简单的道理讲复杂的。从内存的角度去看,JavaScript中的数据类型无非两种,原始类型和引用类型。前者存放在栈内存中,后者存放在堆内存中,两者区别我就不详细说了,这个区别就决定了引用类型的赋值,赋的也是引用类型存放在栈内存中的地址,故也是按值传递,所以只有按值传递这一说,“共享传递”理解上又搞复杂了。详见字节跳动大佬的总结

@CarberryChai
Copy link

以前也不怎么懂,学了c++瞬间就懂了,什么Java JavaScript 中说的引用就是c++的指针,跟c++中所说引用完全不同,c++中的引用只是一个对象的别名,声明时必须绑定一个存在对象,不能解绑。

@biyongzhi110622
Copy link

image
这句话不敢苟同。就算采用引用传递,把o从新赋值成2,外层的值不会跟着修改啊。就好比 var obj = {a:1};var foo = obj;foo = 2;这时候修改foo了,obj不受影响啊

@daixiongsheng
Copy link

这不就C语言的指针嘛,我感觉只要指针理解到位了,什么引用传递,值传递都是弟弟。

@banxian721
Copy link

哈哈,本来没看懂,基于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”
这句话有问题把; 4.形参:创建o快捷方式,但o不指向obj指向的文件夹,却指向了快捷方式obj本身(快捷方式的快捷方式叫高阶快捷方式?哈哈,应该就是就是共享传递的意思吧),形参 o 指向的就是 obj ,此时 obj 和 o 在栈内存中都保存有指向堆内存中的对象 {value:2} 的指针,只是指针不同,但是指向得确是同一个堆内存地址

@che223
Copy link

che223 commented Dec 4, 2021

评论区的各位太强了,真的学习到很多

@malqqin
Copy link

malqqin commented Dec 16, 2021

数据类型分为两种 值类型,引用类型 。但是参数传递 和数据类型没关系,不管是什么数据类型,都是值传递。 你传一个对象,他是引用类型参数,但是从实参到形参的过程,传递方式是值传递,不涉及任何引用,是两个东西。(若非如此,形参赋值一个新对象,原来对象也得变)

@daomingQian
Copy link

就相当于参数赋值给一个变量?
`
let obj = { value: 2 }
var isNStraightHand = function (hand) {
hand = 1
}

var isNStraightHand = function () {
     let hand = obj
     hand = 1
}


isNStraightHand(obj)

`

@cliYao
Copy link

cliYao commented Dec 30, 2021 via email

@lunhui1994
Copy link

lunhui1994 commented Dec 30, 2021 via email

@Sunburst7
Copy link

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

不是呀,要是传递的是引用的值,第三个例子指针就被修改了,就无法指回原来的对象了

@lunhui1994
Copy link

lunhui1994 commented Jan 19, 2022 via email

@supermanklk
Copy link

https://juejin.cn/post/7083119474114560007/

这个文章够清晰

@DaphnisLi
Copy link

所以说,纠结到底是按什么方式传递,没啥用。就记住变量之间的赋值,是浅拷贝,至于浅拷贝原理,这个应该都明白!

@lunhui1994
Copy link

lunhui1994 commented Nov 22, 2022 via email

@cliYao
Copy link

cliYao commented Nov 22, 2022 via email

@Tjdoo
Copy link

Tjdoo commented Aug 2, 2023

例子 1 的内存变化过程(0x10 表示内存地址):
parameter

例子2 的内存变化过程(忘了标注云朵表示堆):
parameter2

例子3 的内存变化过程:
parameter3

@lunhui1994
Copy link

lunhui1994 commented Aug 2, 2023 via email

1 similar comment
@lunhui1994
Copy link

lunhui1994 commented Feb 19, 2024 via email

@cliYao
Copy link

cliYao commented Feb 19, 2024 via email

@rainbowyy
Copy link

rainbowyy commented Feb 19, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests