日期:2014-05-16  浏览次数:20507 次

Javascript 按值和按引用(By Value versus By Reference)

像其他的语言一样,Javascript也包括下列3种重要的使用数据的方式:

1,拷贝,比如说赋予一个新的变量

2,传参,传入一个函数或者方法

3,比较,比较两个数据的值时候相等

想要深入理解一门语言,一定要理解这3种使用数据的方式。

?

在处理数据的时候,最常用的2种方式是按值按引用

在传值的方式下,值本身是最重要的数据:

  1. 用于拷贝的时候,总是重新拷贝一份新的值存放在新的变量或者对象属性或者数组元素中,新值和旧值是完全独立且分开存储的;
  2. 用于传参的时候,总是拷贝一份新的值传入函数,在函数里无论对该拷贝做任何操作都不会影响原始值;
  3. 用于比较的时候,两个截然不同的值需要按字节比较以保证他们是完全相等的。

在传引用的方式下,值只有一份拷贝,操作的总是指向该值的引用。变量不会直接持有该值,而是持有引用。不论是在做赋值,入参还是比较操作的时候,操作的对象总是引用。

  1. 用于拷贝的时候,只是分配指向该值的引用,而不是值的拷贝或者值本身。赋值之后,新的变量指向的和原始变量指向的是同一个值。可以通过任何一个变量来操作原始值,比如说改变了新的变量的值,那么原始变量的值也会随着改变。
  2. 用于传参的时候,函数可以通过该应用修改原始值,并且在函数外面可见。
  3. 用于比较的时候,比较两个变量指向的是不是同一个原始值,指向不同的两个值的两个变量总是不等的。

下面的列表是传值和传引用的对比:

传值(By value) 传引用(By reference)
拷贝(Copy) 拷贝一份新值,他们是两个完全不同且独立的拷贝 只拷贝了引用,如果新的变量值改变了,那么原始的引用也会随之改变
传参(Pass) 一份新的拷贝作为参数传入函数,在函数内改变只是该拷贝,不会影响原始值 引用作为参数传入函数,在函数内改变该引用会造成原始值的改变
比较(Compare) 一般都是按字节比较,以保证两个不同的值是相等的 只要判断两个引用是否指向同一个值,即使字节码相等,两个指向不同的的值的引用依然不等

?

基本类型和引用类型

Javascript处理数据的一个基本原则是,基本类型按值处理,引用类型,顾名思义,按引用来操作。Number和Boolean是基本类型,他们可以容易的被javascript解释器转换成字节码来处理。Object是引用类型,Array和Function是特定类型的Object,他们都是引用类型。这些引用类型可以包含任意的属性或者元素,所以他们无法像基本类型一样转换成字节码来处理。而且对象和数组都可以变得很大,按值操作对他们来说毫无意义,因为如果进行拷贝或者比较操作会占用大量内存。

?

String类型如何处理?

一个字符串可以是任意长度,看起来他们应该按引用来处理。事实上,由于他们不是对象,Javascript经常简单的把他们当成基本类型来处理,但是String不能适配按值或者按引用模型,我们将在后面讨论。

?

下面就让我们用一系列例子来说明按值和按引用的区别。

按值拷贝,传参,比较

// 按值拷贝
var n = 1;  // 变量n持有值1
var m = n;  // 拷贝值: 变量m持有不同的值1

// 按值传参
// 你会发现,函数并没有按我们的要求工作
function add_to_total(total, x)
{
    total = total + x;  // 只是内部的拷贝,total变化只在函数内可见
}

//调用函数,按值传入Number类型n,m
add_to_total(n, m);

//验证结果,发现n并没有变化
if (n == 1) m = 2;  // 把m的值改成2


?按引用拷贝,传参,比较

//创建一个对象类型,xmas是一个指向Date的引用
var xmas = new Date(2007, 11, 25);
// 当拷贝引用的时候,新的变量还是指向原始对象
var solstice = xmas;  // 两个变量指向同一个原始对象

// 通过新的变量改变原始对象
solstice.setDate(21);

// 该改变对于老的引用依然可见
xmas.getDate( );  // 返回21, 而不是原始值25

// 对象和数组同理
// 下面的函数演示给数组每一个元素增加一个值
// 指向数组的引用被当成参数传入函数,而不是数组的拷贝
// 在函数里的改变对于函数外部同样可见
function add_to_totals(totals, x)
{
    totals[0] = totals[0] + x;
    totals[1] = totals[1] + x;
    totals[2] = totals[2] + x;
}

// 按引用比较
// 由于两个引用指向的是同一个对象,所以他们相等
(xmas == solstice)  // 返回true

// 重新创建两个引用,指向同一天
var xmas = new Date(2007, 11, 25);
var solstice_plus_4 = new Date(2007, 11, 25);

//但是按照"按引用比较"原则,他们是不等的!
(xmas != solstice_plus_4)  // 返回 true

?

”按引用传参“的含义:

按引用传参的目的是为了在函数内部对传入的参数进行修改,比如对传入的对象属性或者数组元素进行修改,这个对于函数外部依然是可见的,但是如果开发人员在函数内部用另一个指向对象或者数组的引用重写了传入的引用,那么这个改变在函数外部依然是不可见的。所以有的开发人员就认为,”按引用传参“其实就是“按值传参”,只不过传入的值是引用本身,而不是引用指向的原始对象。

?

下面的例子演示了这个问题:

//这是函数add_to_totals( )的另一个版本,由于在函数内部,开发人员试图直接修改引用,所以该函数并不能正常工作
function add_to_totals2(totals, x)
{
    newtotals = new Array(3);
    newtotals[0] = totals[0] + x;
    newtotals[1] = totals[1] + x;
    newtotals[2] = totals[2] + x;
    totals = newtotals;  // totals在函数外部并没有任何变化
}
?

字符串的拷贝和传参

本文之前提过,字符串无法用按值和按引用的二分法来严格定义。由于字符串不是对象,我们可以很自然的把它当成基本类型,按照基本类型的处理原则,他们应该是按值来处理。但是字符串又可以是任意长度,如果在拷贝,传参和比较操作用都是基于字节码来操作的话,是非常低效的,所以我们又很自然的认为字符串应该按照引用类型来处理。

?

让我们停止无谓的猜测,写段代码了实验吧。如果String是按引用来拷贝,传参的话,我们应该可以通过引用来修改字符串的原始值。

?

在写代码之前,有一个问题困扰着我们,我们没有办法去修改String。如charAt()方法返回传入的字符所在的位置,但是我们找不到相应的setCharAt()方法。这不是设计的缺陷,而是有意为之。在Javascript中,String被故意设计成不可变对象,没有任何javascript语法,函数,或者属性允许你修改字符串。

?

由于String的不可变行,讨论之前的问题已经毫无意义:没有办法判断字符串是按值还是按引用处理。我们可以这么认为,为了提高效率,字符串是按引用处理的,但是这个对于我们写代码并不重要。

?

字符串比较