本章,我们将讲解在ECMAScript向函数function传递参数的策略。
计算机科学里对这种策略一般称为“evaluation strategy”(大叔注:有的人说翻译成求值策略,有的人翻译成赋值策略,通看下面的内容,我觉得称为赋值策略更为恰当,anyway,标题还是写成大家容易理解的求值策略吧),例如在编程语言为求值或者计算表达式设置规则。向函数传递参数的策略是一个特殊的case。
http://dmitrysoshnikov.com/ecmascript/chapter-8-evaluation-strategy/
写这篇文章的原因是因为论坛上有人要求准确解释一些传参的策略,我们这里给出了相应的定义,希望对大家有所帮助。
很多程序员都确信在JavaScript中(甚至其它一些语言),对象是按引用传参,而原始值类型按值传参,此外,很多文章都说到这个“事实”,但有多人真正理解这个术语,而且又有多少是正确的?我们本篇讲逐一讲解。
一般理论
需要注意到,在赋值理论里一般有2中赋值策略:严格——意思是说参数在进入程序之前是经过计算过的;非严格——意思是参数的计算是根据计算要求才去计算(也就是相当于延迟计算)。
然后,这里我们考虑基本的函数传参策略,从ECMAScript出发点来说是非常重要的。首先需要注意的是,在ECMAScript中(甚至其他的语如,C,JAVA,Python和Ruby中)都使用了严格的参数传递策略。
另外传递参数的计算顺序也是很重要的——在ECMAScript是左到右,而且其它语言实现的反省顺序(从右向做)也是可以用的。
严格的传参策略也分为几种子策略,其中最重要的一些策略我们在本章详细讨论。
下面讨论的策略不是全部都用在ECMAScript中,所以在讨论这些策略的具体行为的时候,我们使用了伪代码来展示。
按值传递
按值传递,很多开发人员都很了解了,参数的值是调用者传递的对象值的拷贝(copy of 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}
复制代码
按引用传递
另外一个众所周知的按引用传递接收的不是值拷贝,而是对象的隐式引用,如该对象在外部的直接引用地址。函数内部对参数的任何改变都是影响该对象在函数外部的值,因为两者引用的是同一个对象,也就是说:这时候参数就相当于外部对象的一个别名。
伪代码:
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}
复制代码
该策略可以更有效地传递复杂对象,例如带有大批量属性的大结构对象。
按共享传递(Call by sharing)
上面2个策略大家都是知道的,但这里要讲的一个策略可能大家不太了解(其实是学术上的策略)。但是,我们很快就会看到这正是它在ECMAScript中的参数传递战略中起着关键作用的策略。
这个策略还有一些代名词:“按对象传递”或“按对象共享传递”。
该策略是1974年由Barbara Liskov为CLU编程语言提出的。
该策略的要点是:函数接收的是对象对于的拷贝(副本),该引用拷贝和形参以及其值相关联。
这里出现的引用,我们不能称之为“按引用传递”,因为函数接收的参数不是直接的对象别名,而是该引用地址的拷贝。
最重要的区别就是:函数内部给参数重新赋新值不会影响到外部的对象(和上例按引用传递的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}
复制代码
这个处理的假设前提是大多数语言里用到的对象,而不是原始值。
按共享传递是按值传递的特例
按共享传递这个策略很很多语言里都使用了:Java, ECMAScript, Python, Ruby, Visual Basic等。此外,Python社区已经使用了这个术语,至于其他语言也可以用这个术语,因为其他的名称往往会让大家感觉到混乱。大多数情况下,例如在Java,ECMAScript或Visual Basic中,这一策略也称之为按值传递——意味着:特殊值——引用拷贝(副本)。