跳到主要内容

求值策略

1. 参数的求值策略

var x = 1;
function f(m) {
return m * 2;
}
f(x + 5);

上面代码先定义函数 f,然后向它传入表达式 x + 5。请问,这个表达式应该何时求值?

  1. 一种意见是"传值调用"(call by value),即在进入函数体之前,就计算 x + 5 的值(等于 6),再将这个值传入函数 f。C 语言就采用这种策略。
f(x + 5);
// 传值调用时,等同于
f(6);
  1. 另一种意见是“传名调用”(call by name),即直接将表达式 x + 5 传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。
f(x + 5)(
// 传名调用时,等同于
x + 5
) * 2;

传值调用和传名调用,哪一种比较好?

传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。

function f(a, b) {
return b;
}

f(3 * x * x - 2 * x - 1, x);

上面代码中,函数 f 的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。因此,有一些计算机学家倾向于"传名调用",即只在执行时求值。

2. 按值传递 VS 按引用传递

按值传递(call by value)是最常用的求值策略:函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。

按引用传递(call by reference)时,函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值。

按引用传递会使函数调用的追踪更加困难,有时也会引起一些微妙的 BUG。

按值传递由于每次都需要克隆副本,对一些复杂类型,性能较低。两种传值方式都有各自的问题。

JS 的基本类型,是按值传递的。

var num = 10;
function fn(n) {
n = 10;
}
fn(num);
console.log(num); // 10

再来看对象:

var obj = { num: 100 };
function fn(o) {
o.num = 200;
}
fn(obj);
console.log(obj.num); // 200

这是因为 obj 和 0 都是指向内存中同一地址,o.num 重新赋值也就是 obj.num 重新赋值,所以不是按值传递。但这样是否说明 JS 的对象是按引用传递的呢?我们再看下面的例子:

var obj = { num: 100 };
function fn(o) {
o = 200;
}
fn(obj);
console.log(obj); // {num: 100}

如果是按引用传递,修改形参 o 的值,应该影响到实参才对。但这里修改 o 的值并未影响 obj。 因此 JS 中的对象并不是按引用传递。那么究竟对象的值在 JS 中如何传递的呢?

3. 按共享传递 call by sharing

准确的说,JS 中的基本类型按值传递,对象类型按共享传递的(call by sharing,也叫按对象传递、按对象共享传递)。最早由 Barbara Liskov. 在 1974 年的 GLU 语言中提出。该求值策略被用于 Python、Java、Ruby、JS 等多种语言。

该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。如下面例子中,不可以通过修改形参 o 的值,来修改 obj 的值。

var obj = { x: 1 };
function foo(o) {
o = 100;
}
foo(obj);
console.log(obj.x); // 仍然是1, obj并未被修改为100.

然而,虽然引用是副本,引用的对象是相同的。它们共享相同的对象,所以修改形参对象的属性值,也会影响到实参的属性值。

var obj = { x: 1 };
function foo(o) {
o.x = 3;
}
foo(obj);
console.log(obj.x); // 3, 被修改了!

对于对象类型,由于对象是可变(mutable)的,修改对象本身会影响到共享这个对象的引用和引用副本。而对于基本类型,由于它们都是不可变的(immutable),按共享传递与按值传递(call by value)没有任何区别,所以说 JS 基本类型既符合按值传递,也符合按共享传递。

var a = 1; // 1是number类型,不可变
var b = a;
b = 6; // a = 1

据按共享传递的求值策略,a 和 b 是两个不同的引用(b 是 a 的引用副本),但引用相同的值。由于这里的基本类型数字 1 不可变,所以这里说按值传递、按共享传递没有任何区别。

4. 基本类型的不可变(immutable)性质

基本类型是不可变的(immutable),只有对象是可变的(mutable)。

在 JS 中,任何看似对 string 值的”修改”操作,实际都是创建新的 string 值。

var str = 'abc';
str[0]; // "a"
str[0] = 'd';
str; // 仍然是"abc";赋值是无效的。没有任何办法修改字符串的内容

而对象就不一样了,对象是可变的。

var obj = { x: 1 };
obj.x = 100;
var o = obj;
o.x = 1;
obj.x; // 1, 被修改
o = true;
obj.x; // 1, 不会因o = true改变

这里定义变量 obj,值是 object,然后设置 obj.x 属性的值为 100。而后定义另一个变量 o,值仍然是这个 object 对象,此时 obj 和 o 两个变量的值指向同一个对象(共享同一个对象的引用)。所以修改对象的内容,对 obj 和 o 都有影响。但对象并非按引用传递,通过 o = true 修改了 o 的值,不会影响 obj。