[笔记补完计划]理解JavaScript的参数传递
1 前言
最近整理文件时发现了以前写的很多笔记,恰好想到Blog已经很长时间没更新了。
干脆把这些笔记再整理润色一下,搞点配图(当年一位大牛曾经说过:“我总算知道为什么我博客写的那么短了,因为图少”)慢慢都发出来吧。
今天是第一篇。
2 两个例子
先来看两个例子,选自《JavaScript 高级程序设计(第三版)》P70-71页,第四章-4.1节-4.1.3小节。
2.1 例子1 基本类型的参数传递
1 2 3 4 5 6 7 8 |
function addTen(num){ num += 10; return num; } var count = 20; var result = addTen(count); alert(count); //20,没有变化 alert(result); //30 |
书上的解释是:JavaScript参数传递都是按值传递。
根据这个说法,传递给函数addTen
的值是 20 ,所以函数结束时,原始变量count
并不会发生改变。
2.2 例子2 引用类型的参数传递
1 2 3 4 5 6 7 8 |
function setName(obj) { obj.name = "Nicholas"; obj = new Object(); obj.name = "Greg"; } var person = new Object(); setName(person); alert(person.name); //"Nicholas" |
这里产生了疑问:
obj.name = "Nicholas";
这一步操作给person
添加了name
属性并赋值Nicholas
,在局部作用域中对对象的修改,反映在了全局上,这是按引用传递的么?- 如果是按引用传递的,那么
person
的name
属性应该是Greg
了,那么为什么最终person
的name
属性还是那么person
的name
属性呢?
3 理解参数传递
3.1 例子3 整合例子
为了解答前面的两个疑问,先把书上提供的例子整合修改一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function changeAttribs(num, obj1, obj2) { num += 10; obj1.name = "bob"; obj2 = {name: "carl"}; } var count = 20; var person1 = {name:"abby"}; var person2 = {name:"abby"}; changeAttribs(count, person1, person2); console.log(count); console.log(person1.name); console.log(person2.name); |
最终的结果是:
1 2 3 |
20 bob abby |
结果有点出乎意料,那么先来了解一下什么是按值传递(传值调用 Call-by-value)和按引用传递(引用调用 Call-by-reference)。
3.2 传值调用
传值调用也被叫做按值传递,在传值调用中,传递给函数的参数是函数调用时实参的拷贝,在传值调用中实际参数被求值,其值被绑定到函数中对应的变量上。
通常的做法是把值复制到新的内存区域。
即例子3中函数changeAttribs
的三个形参num
、obj1
、obj2
是调用函数时传递的实参count
、person1
、person2
的拷贝。那么,无论num
、obj1
、obj2
怎么变化,count
、person1
、person2
都保持不变。
问题来了,person1
变了。
3.3 引用调用
引用调用也被叫做按引用传递,在引用调用中,传递给函数的参数是函数调用时实参的隐式引用而不是拷贝。
通常函数能够修改这些参数(例如赋值),而且改变对调用者是可见的。
即例子3中函数changeAttribs
的三个形参num
、obj1
、obj2
是调用函数时传递的实参count
、person1
、person2
的引用(一一对应指向同一块内存空间)。那么,函数内对num
、obj1
、obj2
的任何修改都反映到count
、person1
、person2
上。
问题又来了,num
和obj2
没有改变。
主要问题集中在JS的引用类型上面。
3.4 传共享调用
在传共享调用中,传递给函数的参数是函数调用时对象实参的引用的拷贝,也就是对象变量指针的拷贝。
即例子3中函数changeAttribs
调用时实参中是对象变量的,形参得到的是对象变量指针的拷贝。也就是形参和实在指向了同一个对象,函数体内部对对象的修改对调用者可见。
4 代码分析
4.1 小动画解释
看个动图就懂了,觉得慢或者看不清楚的,往下走,有文字版。
4.2 文字配图解释
4.2.1 变量初始化
1 2 3 |
var count = 20; var person1 = {name:"abby"}; var person2 = {name:"abby"}; |
4.2.2 调用函数
1 |
changeAttribs(count, person1, person2); |
4.2.3 执行函数体
1 2 3 |
num += 10; obj1.name = "bob"; obj2 = {name: "carl"}; |
如图所示:
变量 num
的值的改变,并不会影响到变量 count
。
变量 obj1
和变量person1
指向了堆内存中同一个对象,所以当执行到obj1.name = "bob";
时,变量person1
随之改变。
变量obj2
重新赋值了,指向了函数体内创建的局部对象变量,所以修改obj2
并不会对变量person2
产生影响。
5 结论
从上面的分析,可以回答第二节提出的两个问题。
1.obj.name = "Nicholas";
这一步操作给person
添加了name
属性并赋值Nicholas
,在局部作用域中对对象的修改,反映在了全局上,这是按引用传递的么?
答:不是按引用传递的。
2.如果是按引用传递的,那么person
的name
属性应该是Greg
了,那么为什么最终person
的name
属性还是那么person
的name
属性呢?
答:因为不是引用传递,是传共享调用,函数的形参获得的是实参对象的指针的值的拷贝,指向了堆内存中的同一个对象。在函数体内声明的新对象赋值给形参时,形参实际上指向了新的对象,所以并不会影响到实参。
那么,对于JS来说:
- 基本类型是传值调用
- 引用类型是传共享调用
传值调用本质上就是对变量的值的拷贝。
传共享调用本质上是传递对象的指针的拷贝。
由于传递对象的指针本身也是值,所以传共享调用也可以当作传值调用,从这个角度上看,《JavaScript 高级程序设计(第三版)》上说: “JavaScript参数传递都是按值传递",是有道理的。
而《你所不知道JavaScript(中卷)》第二章 第28-29页中又说:“复合值——对象和函数,则总是通过引用复制的方式来赋值/传递。”
看来,目前JavaScript到底是什么参数传递方式,尚有争论,把实现机制搞明白就好了,叫名字并不重要。