九、JavaScript之参数按值传递

定义

在《JavaScript 高级程序设计》中讲到传递参数:

ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。对很多开发者来说,这一块可能会不好理解,毕竟变量有按值和按引用访问,而传参则只有按值传递。

按值传递

在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或者用 ECMAScript 的话说,就是 arguments 对象中的一个槽位)。
来看一个例子:

1
2
3
4
5
6
7
function foo(num) {
num += 10;
console.log(num); // 30
}
var count = 20;
foo(count);
console.log(count); // 20,没有变化

这里,函数 foo()有一个参数 num,它其实是一个局部变量。在调用时,变量 count 作为参数传入。count 的值 20 被复制给参数 num 。在函数内部,参数 num 的值被加上了 10,但这不会影响函数外部的变量 count。参数 num 和变量 count 互不干扰,它们只不过碰巧保存了一样的值。

引用传递

众所周知按引用传递接收的不是值拷贝,而是对象的隐式引用,如该对象在外部的直接引用地址。函数内部对参数的任何改变都是影响该对象在函数外部的值,因为两者引用的是同一个对象,也就是说:这时候参数就相当于外部对象的一个别名。
伪代码:

1
2
3
4
5
6
7
8
9
var obj = {
value: 1,
};
function foo(o) {
o.value = 2;
console.log(o.value); //2
}
foo(obj);
console.log(obj.value); // 2

哪ECMAScript 中函数传递引用数据类型参数是引用传递吗?然而并不是,除了上面两种策略以外,我们看看第三种策略。

共享传递

共享传递策略还有一些代名词:“按对象传递”或“按对象共享传递”。该策略是1974年由Barbara Liskov为CLU编程语言提出的,策略的要点是:

函数接收的是对象引用的拷贝(副本),该引用拷贝和形参以及其值相关联。
这里出现的引用,我们不能称之为“按引用传递”,因为函数接收的参数不是直接的对象别名,而是该引用地址的拷贝。

最重要的区别就是:函数内部给参数重新赋新值不会影响到外部的对象(和上例按引用传递的case),但是因为该参数是一个地址拷贝,所以在外面访问和里面访问的都是同一个对象,改变该参数对象的属性值将会影响到外部的对象。

1
2
3
4
5
6
7
8
9
var person = {
num: 1,
};
function foo(obj) {
obj.num = 2;
}

foo(person);
console.log(person.num); // 2

这里,我们创建了一个对象 person 并这个对象被传给 foo 方法,并被复制到参数 obj 中。在函数内部,obj 和 person 都指向同一个对象。个人觉得引用数据类型传递的也是值, 不过这个值是一个地址指针,
我们再来看看下面这个修改后的例子:

1
2
3
4
5
6
7
8
9
10
11
12
var person = { num: 1 };

function foo(obj) {
obj.num = 2;
// 将obj引用指针指向了新的一个对象上
obj = new Object();
obj.num = 3;
}
console.log(person.num); // 1
foo(person);
// 如果这里是按照引用传递,那么,下面的输出应该是3
console.log(person.num); // 2

当 person 传入 foo()时,其 num 属性被设置为 2。然后变量 obj 被设置为一个新对象且 num 属性被设置为 3。如果 person 是按引用传递的,那么 person 应该自动将指针改为指向 num 为 3 的对象。可是,当我们再次访问 person.num 时,它的值是 2,这表明函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。

总结

  1. 如果参数是基本类型,那么是按值传递。
  2. 如果参数是引用数据类型,传递的还是值,但是这个值是引用数据类型地址的拷贝。