通知
此博客运行在jpress系统上,如果你喜欢此博客模板,请加QQ群:1061691290(whimurmur模板/jpress插件),免费下载使用
文章来源于网络,无法注明出处的还请谅解,如果出处注明错误(如仍是载转),请联系我修改

深入JavaScript 共享传递(转载)

716人浏览 / 0人评论 | 这是对我有帮助的文章  | 分类: 计算机语言  | 标签: 转载  | 

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

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

什么是按值传递呢?

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

如果你没有学过C/C++/C#等编程语言,对堆栈以及汇编并不是很了解,那么阅读《一、从小白角度讲》;反之,请看《二、从底层角度讲》,这也有助于你理解共享传递的本质。


一、从小白角度讲

这种讲解方式是由于 你没有学过C/C++/C#等编程语言,对堆栈以及汇编也并不是很了解,所以按下面这么讲,比较助于你理解。

很多人会说,我记得:“函数参数 如果是基本数据类型,就按值传递;如果是对象就引用传递” 其实,JS函数参数 应该共享传递的.。但由于共享传递拷贝的是引用的副本,也叫拷贝,所以在高程中也直接认为是按值传递了。

1、按值传递


例:

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

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

2、看似 引用传递(实则共享传递)


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

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

例:

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

3、共享传递

例:

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

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

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

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

最后你可以这么理解:
在JS中,参数如果是基本类型是按值传递,如果是引用类型按共享传递。
但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。


二、从底层角度讲


如果你对堆栈汇编有了一些了解,好吧,下面你也应该能看的懂。

我们知道JavaScript的引擎是C++实现的, 而在这一块概念上C#与C++又是大致一样的,所以下面我们会用C#与JS对比说明。(不懂C#不要紧,会C也OK)

C#的数据类型分为2种: 值类型和引用类型, 方法参数的传递方式也分为2种: 值传递和引用传递, 这里要强调的是数据类型和方法参数的传递方式没有半毛钱关系. 这两者排列组合后得到4种情况:

(1)方法参数类型是值类型, 用值传递;
(2)方法参数类型是引用类型, 用值传递;
(3)方法参数类型是值类型, 用引用传递;
(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, 并在这里保存在托管堆中那个对象的内存地址(我们可以把这个内存地址简化理解成指向对象实例的指针). 这就是值类型和引用类型的区别。


回过来再看上面《 一、从小白角度讲》"按值传递"的例子, 这个例子符合方法参数是值类型并用值传递的情况。

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

value是值类型,它在内存栈中的地址001保存了1这个数值,
在foo(value); 这句,value是实参,而foo函数声明中的v是形参,js引擎在内存栈中为形参v分配了一个地址002, 其中也保存了1这个值, 这时修改v的值, 是修改内存地址002里的值, 而地址001里的值没变, 所以在foo函数执行完, 再打印value时, 依然是1。


接下来看上面《 一、从小白角度讲》“引用传递” 的例子。

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

正确的说法应该是引用类型并用值传递.
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。


再看上面《 一、从小白角度讲》“共享传递” 的例子。

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

这个例子依然是值传递, 唯一与C#不同的是, C#的变量类型定义后不能改变, 而JS的变量类型是可以随意改变的, 因此这个例子无法跟C#中的值传递来类比.
再来分析这个例子, 首先obj实例化一个对象, 有一个属性value, 值为1, 在内存中就是现在内存堆中分配一个内存空间, 其地址为022, 保存了一个对象(包括它的属性value和值1), 然后再到内存栈中分配一个内存地址021, 保存了内存地址022这个值。
在foo(obj);这句, obj是实参, 而o是形参, 这时在内存栈中给形参o分配了一个地址023, 也保存022这个值 ( 这里因为调用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#中的引用类型的值传递和引用类型的引用传递的对比。简单来说, 引用类型的值传递, 在方法内部如果对形参重新赋值, 哪怕是同一个类的对象, 在赋值后修改对象的属性, 实参的对应的属性值都不会改变, 同时实参指向的对象也不变, 而形参在重新赋值后已经指向一个新的对象了; 而引用类型的引用传递, 在方法内部如果对形参重新赋值, 那么实参也跟着重新赋值, 实参最初所指向的那个对象将不被任何变量所指向。


上面是用C#与JS对比举例说明,但再说一遍:在JS中,参数如果是基本类型是按值传递,如果是引用类型按共享传递。


最后举个例子:

var a = {val: 12} ;   
var b = a;
//这里变量a,b 保存的值 都是{val: 12}对象的地址

如果直接修改b的保存值(即直接修改b保存的{val: 12}的地址),则不影响a,因为b是相当于引用类型的副本。如下:

b = 2;
console.log(a);  // {val: 12}

如果是通过b保存对象的地址来访问修改其对象属性,则影响a。(因为相当于间接修改了{val: 12}对象,而a指向这该对象)
如下:

b.val = 0;
console.log(a);  // {val: 0}

所以如果我们探其了本质,你会问: “共享传递是什么?”
无非就是某某大佬起的一个专业术语,一个名字而已。


亲爱的读者:有时间可以点赞评论一下

点赞(0) 打赏

全部评论

还没有评论!