求值策略(js中参数是按值传递还是按引用传递)
- 求值策略(js中参数是按值传递还是按引用传递)
- 前言
- 求值策略(evaluation strategy)
- 1.按值传递(call by value)
- 2.按引用传递(call by reference)
- 3.按共享传递(copy of sharing)
- 4.三者总结
- ECMAscript实现
- 1.声明变量时不同的内存分
- 1.1原始值
- 1.2引用值
- 2.复制变量时不同
- 2.1原始值
- 2.2引用值
- 3.向函数传递参数时不同
- 3.1原始值
- 3.2引用值
- 特声明
- 参考链接
前言
其实之前在看高程这本书的时候就看到过js传值的时候是按值传递还是按引用传递这一问题,按值传递理解起来很简单,但是按引用传递就有点模糊,并不是说原来多么难理解,而是当时根本就没有把按引用传递这个概念弄清楚,所以在了解任何问题之前,一定要先弄清楚概念和术语,再谈深入理解。
所以本篇文章是由高程一书的第四章节引发了一系列的血案:
求值策略(evaluation strategy)
在计算机科学里面,有一部分叫求值策略。它决定变量之间,函数调用时实参和形参之间值是如何传递的。
需要多说一点的是,在求值理论里一般有两种求值策略:
严格–参数在键入程序之前是经过计算过的
非严格–参数计算是根据计算要求才去计算的
然后,这里我们考虑基本的函数传参策略,从ECMAScript出发点来说是非常重要的。首先需要注意的是,在ECMAScript中(甚至其他的语如,C,JAVA,Python和Ruby中)都使用了严格的参数传递策略。
另外传递参数的计算顺序也是很重要的——在ECMAScript是左到右,而且其它语言实现的反省顺序(从右向做)也是可以用的。
严格的传参策略也分为几种子策略,其中最重要的一些策略我们在本章详细讨论。
下面讨论的策略不是全部都用在ECMAScript中,所以在讨论这些策略的具体行为的时候,我们使用了伪代码来展示。
1.按值传递(call by value)
函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。一般来说,是重新分配了新内存(我们不关注分配内存是怎么实现的——也是是栈也许是动态内存分配),该新内存块的值是外部对象的拷贝,并且它的值是用到函数内部的。
bar = 10
procedure foo(barArg):
barArg = 20;
end
foo(bar)
// foo内部改变值不会影响内部的bar的值
print(bar) // 10
但是,如果该函数的参数不是原始值而是复杂的结构对象是时候,将带来很大的性能问题,C++就有这个问题,将结构作为值传进函数的时候——就是完整的拷贝。
我们来给一个一般的例子,用下面的赋值策略来检验一下,想想一下一个函数接受2个参数,第1个参数是对象的值,第2个是个布尔型的标记,用来标记是否完全修改传入的对象(给对象重新赋值),还是只修改该对象的一些属性。
// 注:以下都是伪代码,不是JS实现
bar = {
x: 10,
y: 20
}
procedure foo(barArg, isFullChange):
if isFullChange:
barArg = {z: 1, q: 2}
exit
end
barArg.x = 100
barArg.y = 200
end
foo(bar)
// 按值传递,外部的对象不被改变
print(bar) // {x: 10, y: 20}
// 完全改变对象(赋新值)
foo(bar, true)
//也没有改变
print(bar) // {x: 10, y: 20}, 而不是{z: 1, q: 2}
2.按引用传递(call by reference)
函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值。简而言之,引用相当于是外部变量的别名,实际操作的就是该变量,即在函数内对该变量进行修改的话,在外部该变量也会相应被修改。
procedure foo(barArg, isFullChange):
if isFullChange:
barArg = {z: 1, q: 2}
exit
end
barArg.x = 100
barArg.y = 200
end
// 使用和上例相同的对象
bar = {
x: 10,
y: 20
}
// 按引用调用的结果如下:
foo(bar)
// 对象的属性值已经被改变了
print(bar) // {x: 100, y: 200}
// 重新赋新值也影响到了该对象
foo(bar, true)
// 此刻该对象已经是一个新对象了
print(bar) // {z: 1, q: 2}
3.按共享传递(copy of sharing)
调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:函数内部给参数重新赋新值不会影响到外部的对象(和上例按引用传递的case),但是因为该参数是一个地址拷贝,所以在外面访问和里面访问的都是同一个对象(例如外部的该对象不是想按值传递一样完全的拷贝),改变该参数对象的属性值将会影响到外部的对象。
注意:这里出现的引用,我们不能称之为“按引用传递”,因为函数接收的参数不是直接的对象别名,而是该引用地址的拷贝。
procedure foo(barArg, isFullChange):
if isFullChange:
barArg = {z: 1, q: 2}
exit
end
barArg.x = 100
barArg.y = 200
end
//还是使用这个对象结构
bar = {
x: 10,
y: 20
}
// 按贡献传递会影响对象
foo(bar)
// 对象的属性被修改了
print(bar) // {x: 100, y: 200}
// 重新赋值没有起作用
foo(bar, true)
// 依然是上面的值
print(bar) // {x: 100, y: 200}
4.三者总结
- 按值传递:就是把值的拷贝传递进去,形参的改变不会影响实参。
- 按引用传递:就是把地址(这个地址存在变量中,这个地址指向堆中的对象)传递进去,操作形参就是操作实参,
对形参重新赋值,会影响实参。 - 按共享传递:就是把地址的拷贝传递进去,实参和形参中的地址同时指向对象,所以形参对对象的属性的操作,通 过实参都能反映出来,但是对形参重新赋值,不会影响到实参。
ECMAscript实现
在ECMAscript中,变量可以存在两种类型,即原始值和引用值
原始值:
存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。
引用值:
存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存处。
1.声明变量时不同的内存分
1.1原始值
存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值。
1.2引用值
存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。
2.复制变量时不同
在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。而原始类型的值则是可以直接访问到的。
2.1原始值
在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
var num1=5;
var num2=num1;
2.2引用值
在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。(这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)
var obj1=new Object();
var obj2=obj1;
obj1.name="lss"
alert(obj2.name)//lss
3.向函数传递参数时不同
首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。但是为什么涉及到原始类型与引用类型的值时仍然有区别呢,还不就是因为内存分配时的差别。 (我对比了一下,这里和复制变量时遵循的机制完全一样的嘛,你可以简单地理解为传递参数的时候,就是把实参复制给形参的过程)
3.1原始值
按值传递的。
var x="实参";
function foo(x) {
console.log(x); //实参
x="这是形参,我在函数内部修改了这个值";
console.log(x); //这是形参,我在函数内部修改了这个值
}
foo(x);
console.log(x);//实参
由于例子看出,在js中,函数内部修改了形参,并不会影响外面的实参,就是因为函数内部的形参只是一个副本。
3.2引用值
在js中,引用值是按共享传值的。
var obj={
name:"我是obj"
};
function foo(obj) {
console.log(obj);//{name: "我是obj"}
obj.name="我要在函数内部改变";
obj.info="我要在函数内部给obj添加一个属性";
console.log(obj);//{name: "我要在函数内部改变", info: "我要在函数内部给obj添加一个属性"}
}
foo(obj);
console.log(obj);//{name: "我要在函数内部改变", info: "我要在函数内部给obj添加一个属性"}
看了这段代码,你内心肯定想,c,这不就是按引用传递嘛,根本不是书上所说的按值传递啊,因为改变函数内部的对象,外部的对象也给变了。其实不然,千万别被假象所迷惑,天真的孩子们啊,如果js中引用类型真的是按引用传值的话,那么,在函数内部给对象重新赋值,那么函数外部的对象也会被重新赋值,因为按引用传值,实际上就是操作在外部定义的obj这个对象本身。让我们来看一看在js中是不是这样的。
var obj={
name:"我是obj"
};
function foo(obj) {
console.log(obj);//{name: "我是obj"}
//我要给函数内部的对象重新赋值
obj={
info:"我是新的值"
};
console.log(obj);//{info: "我是新的值"}
}
foo(obj);
console.log(obj);//{name: "我是obj"}
看见没有,在函数内给对象赋值了新值,函数外部的对象纹丝不动啊,分分钟举例就反驳了你的认知吧。所以在js中,引用类型是按共享传值的,传递给函数的只是对象在栈中保存的地址(这个地址指向在堆中存储的对象),原理就跟引用类型复制一样。
此外,如果只考虑ECMA-262标准所提供的抽象层次,我们在算法里看到的只有“值”这个概念,实现传递的“值”(可以是原始值,也可以是对象),但是按照我们上面的定义,也可以完全称之为“按值传递”,因为引用地址也是值。
特声明
本文是关于其他优秀博文的总结,也好供自己以后回顾和复习,理清其中概念,共勉之,有不足之处望各位小主指出,我定改之,臣退了。
参考链接
https://www.jb51.net/article/60568.htm
http://www.w3school.com.cn/js/pro_js_value.asp