黄子涵

5.2 变量与引用

对象的概念很好地说明了变量是一种拥有名称的客体。对象本身是没有名称的,之所以使用变量,是为了通过某个名称来称呼这样一种不具有名称的对象。

var hzh = {} // 将对象赋值给变量hzh

变量又分为基本类型的变量(值型变量)与引用类型的变量。由于在 JavaScript 中,变量是不具有类型的,因此从语法标准上来看,两者并没有什么不同。不过,在 JavaScript 中仍然有对象的引用这一概念。

所谓“引用”,可以认为是一种用于指示出对象的位置的标记。如果你熟悉 C 语言,把它理解为是和指针等价的东西也没有问题。不过,引用不支持那些可以对指针进行的运算。引用这一语言功能只有指示位置信息的作用。准确地说,对象的赋值其实是将对象的引用进行赋值。

为了更好地解释引用这一概念,这里对引用类型的变量和值型变量进行比较。将基本类型的值赋值给变量的话,变量将把这个值本身保存起来。这时,可以将变量简单地理解为一个装了该值的箱子。变量本身装有所赋的这个值,所以能够将该值从变量中取出。如果在右侧写上一个变量,这一变量的值将被复制给赋值目标处(左侧)的变量。

var a = 123;    // 将数值123赋值给变量a
var b = a;      // 将变量a的值(数值123)赋值给变量b

像下面这样,对变量 b 进行自增操作后,变量 a 的值是不会发生改变的。图 5.1 对这一执行方式作了说明

var a = 123;    // 将数值123赋值给变量a
var b = a;      // 将变量a的值(数值123)赋值给变量b
console.log("第一次输出变量a的值:");
console.log(a); 
console.log("第一次输出变量b的值:");
console.log(b); 
b++;
console.log("");
console.log("第二次输出变量a的值:");
console.log(a); 
console.log("第二次输出变量b的值:");
console.log(b); 
[Running] node "e:\HMV\JavaScript\JavaScript.js"
第一次输出变量a的值:
123
第一次输出变量b的值:
123

第二次输出变量a的值:
123
第二次输出变量b的值:
124

[Done] exited with code=0 in 0.316 seconds

另一方面,如果将一个对象赋值给变量,其实是把这个对象的引用赋值给了该变量。对象本身是无法赋值给一个变量的。如果在右侧写上了这样的变量,该变量所表示的引用将被复制给赋值目标处(左侧)的变量。对象本身并不会被复制。

var a = { x:1, y:2 }; // 将对象的引用赋值给变量a
var b = a;            // 将变量a的值(对象的引用)赋值给变量b

图5.1 值型变量的执行方式

image

图5.2 引用类型的变量的执行方式

image

如果像下面这样,改变了变量 b 所引用的对象,那么这一改变也会体现在变量 a 之中,这是因为这两个变量通过引用而指向了同一个对象。图 5.2 对这种执行方式进行了说明:

var a = { x:1, y:2 }; // 将对象的引用赋值给变量a
var b = a;            // 将变量a的值(对象的引用)赋值给变量b
console.log("输出变量a的值:");
console.log(a);
console.log("输出变量b的值:");
console.log(b);
console.log("");
b.x++;            // 改变变量b所引用的对象
console.log("输出变量b的x属性:");
console.log(b.x); // 变量b所引用的对象
console.log("输出变量a的x属性:");
console.log(a.x); // 可以发现变量a所引用的对象也被改变
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出变量a的值:
{ x: 1, y: 2 }
输出变量b的值:
{ x: 1, y: 2 }

输出变量b的x属性:
2
输出变量a的x属性:
2

[Done] exited with code=0 in 0.323 seconds

在比较了这两种赋值后,你可能会错误地认为对于值型变量而言,变量值的改变对于其他的变量来说是不可见的,而对于引用类型的变量,这一改变则是可见的。这是一种不正确的理解。对于引用类型的变量,整个过程中发生改变的其实是其引用的对象,而不是该变量的值。引用类型的变量具有的值就是引用(值),这个值将在赋值的时候被复制。请看下面的代码以及图 5.3。

var a = { x:1, y:2 };
var b = a;        // 变量a与变量b引用的是同一个对象
a = { x:2, y:2 }; // 改变了变量a的值(使其引用了另一个对象)
console.log("输出变量b的x属性:");
console.log(b.x); // 变量b所引用的对象没有发生改变
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出变量b的x属性:
1

[Done] exited with code=0 in 0.267 seconds

图5.3 引用类型的变量的执行方式

image

在 JavaScript 中,赋值运算总是会把右侧的值复制给左侧。对于引用类型的变量来说也是一样,会将引用(用于指示对象的一种值)赋值给左侧。函数调用过程中的参数也是这样的执行方式。

5.2.1 函数的参数(值的传递)

代码清单 5.1 是一个典型的例子,hzh_no_swap 函数的代码试图交换所传递的两个参数 hzh_a 与 hzh_b 的值。然而,即使调用了这个函数,也不会对实参 hzh1 和 zero 的值造成任何影响。可以认为,在调用函数时执行了相当于 hzh_a=hzh1 以及 hzh_b=hzh2 的两次赋值操作。虽然变量 hzh1 与 hzh2 是引用类型的变量,但实际上也只是对其引用进行了复制操作。因此,并无法实现对 hzh1 和 hzh2 所引用的对象的交换。

代码清单 5.1 一个无法交换其两参数的值的函数
var hzh1 = 1;
var hzh2 = 2;

console.log("交换之前先打印hzh1和hzh2的值:");
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
console.log("");

function hzh_no_swap(hzh_a, hzh_b) {
    var hzh_tmp = hzh_a;
    hzh_a = hzh_b;
    hzh_b = hzh_tmp;
    console.log("hzh1 = " + hzh_a);
    console.log("hzh2 = " + hzh_b)
}

console.log("调用hzh_no_swap()函数之后:");
hzh_no_swap(hzh1, hzh2);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
交换之前先打印hzh1和hzh2的值:
hzh1 = 1
hzh2 = 2

调用hzh_no_swap()函数之后:
hzh1 = 2
hzh2 = 1

[Done] exited with code=0 in 0.739 seconds

【评】这里的实验结果和书上说的不一样。

在 JavaScript 中,应该把赋值运算看作将右侧的值复制给左侧的一种操作。而这一原则,对于调用函数过程中,参数对引用进行复制的情况也是成立的。这样的规则被称为按值传递(call-by-value)。

在支持对引用或指针进行运算的语言中,可以以代码清单 5.1 中函数的形式,来对实参的值进行交换。JavaScript 不支持这样的功能,所以必须通过其他方式来实现对两个参数值的交换。可以通过传递一个数组并交换其中的元素,或者通过传递一个对象并交换其属性值之类的形式来实现。代码清单 5.2 使用了 JavaScript 自带的增强功能,将交换结果设为函数的返回值,这可以说是一种最为简单的实现代码。

代码清单5.2 一个能够交换两个参数的值的函数(JavaScript 自带的增强功能)
function hzh_swap(hzh_a, hzh_b) {
    return [hzh_b, hzh_a];
}
var hzh1 = 1;
var hzh2 = 2;
console.log("交换之前输出hzh1和hzh2的值:");
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
console.log("");
[hzh1, hzh2] = hzh_swap(hzh1, hzh2);
console.log("输出交换后的值:");
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
交换之前输出hzh1和hzh2的值:
hzh1 = 1
hzh2 = 2

输出交换后的值:
hzh1 = 2
hzh2 = 1

[Done] exited with code=0 in 0.36 seconds

5.2.2 字符串与引用

即使字符串型在内部是以引用类型的方式实现的,从语言规则上来看它仍然是一种值的类型。不过以字符串对象(String 类的对象实例)赋值的变量,从语言规则上来看则是一种引用类型。

5.2.3 对象与引用相关的术语总结

在将对象的引用赋值给变量 a 时,这个对象将被称作“对象a”。这种称法,会有一种(本应不具有名字的)对象其实具有 a 这样一个名称的感觉。显然这样的感觉是不正确的,因为这个对象即使在没有变量 a 的情况下,也能够独立存在。这样说的证据是,如果将变量 a 消去,或是将变量 a 指向其他的对象,原来的这个对象仍然会存在。话虽如此,每次都很准确地使用“变量 a 所引用的对象”这样的说法过于冗长,所以方便起见,还是称其为对象 a。事实上没有被任何变量引用的对象是会被内存自动回收的,不过这已经是另一个话题了。

此外,在上下文不会发生误会的情况下,可以用“对象”这一术语来指代“对象的引用”。对象是一个实体,而引用是用于指示这一实体的位置信息,两者本应是不同的。不过根据上下文可以知道,“将对象赋值给变量 a”的说法很显然是指将对象的引用赋值,所以方便起见可以直接这么说。

posted @ 2022-05-28 09:27  黄子涵  阅读(137)  评论(0编辑  收藏  举报