JS中函数的参数是按值传递还是按引用传递?

《JavaScript高级程序设计(第3版)》中P70页关于传递参数一上来就各种强调开发人员会错误的认为参数是按引用传递,前后看了几遍,甚是迷惑,被几个概念绕得晕乎,说的是玄之又玄,于是花了一下午准备把这个点的所有概念理清。

一、两种不同数据类型的值

  • 基本类型值,指简单的数据段,对于undefined、null、boolean、number、string这5种简单数据类型可以直接操作保存在变量中的实际值,也就是按值访问。

  • 引用类型值,指那些可能由多个值构成的对象,只能操作对象的引用而不是实际的对象,所以要得到引用类型这种值只能按引用访问。

二、声明变量时不同的内存分配

  • 原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置
    这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 — 栈中,这样存储便于迅速查寻变量的值。

  • 引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。
    这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

JS中函数的参数是按值传递还是按引用传递?_第1张图片
内存分配差异

不同的内存分配机制也带来了不同的访问机制:

在ECMAScript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是按引用访问。而原始类型的值则是可以直接访问到的。

三、复制变量时的不同

  • 原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
JS中函数的参数是按值传递还是按引用传递?_第2张图片
原始值的复制
  • 引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量(因为这个引用值只能按引用访问得到),也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。

    这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了。

JS中函数的参数是按值传递还是按引用传递?_第3张图片
引用值的复制

四、函数参数的传递

现在是难理解的地方了。
首先明确一点:ECMAScript中所有函数的参数都是按值来传递的。但是为什么涉及到原始类型与引用类型的值时仍然有区别呢,还不就是因为内存分配时的差别。

(我对比了一下,这里和复制变量时遵循的机制完全一样的,你可以简单地理解为传递参数的时候,就是把实参复制给形参的过程,下面会有代码演示)

  • 原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响,这个很好理解,不再赘述。

  • 引用值:对象变量里面的值是这个对象在堆内存中的内存地址,因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。

JS中函数的参数是按值传递还是按引用传递?_第4张图片
函数参数的传递

用代码解释可能理解更形象:
注:代码范例来自JS高程P71。

function setName(obj) {
    obj.name="Nicholas";
    obj=new Object( );
    obj.name="Greg";
}
var person=new Object( );
setName(person); 

//根据上述函数参数的特点,可以利用arguments对象来重写这个函数:
function setName( ) {
    arguments[0].name="Nicholas";
    arguments[0]=new Object( );
    arguments[0].name="Greg";
}

//将参数person传入setName函数,函数执行步骤入下:
function setName(person){
    arguments[0]=person; //将arguments[0]和person指向同一个对象
    arguments[0].name="Nicholas"; //也即person.name="Nicholas"
    arguments[0]=new Object; //重新定义arguments[0]。这时,arguments[0]已经跟 person“脱钩”
    arguments[0].name="Greg"; //这行代码跟person已经没有关系了
}

//结果自然很明朗,没悬念
alert(person.name); // "Nicholas";

五、总结

  • 在ECMAScript中,所谓参数按值传递就是,将形式参数初始化为实际参数的值。或者说将所传递的实际参数的值复制到函数内部的arguments数组中。对于基本类型值,就是在函数内部创建了一个该值的副本;对于引用类型值则复制了一个指向某个对象的指针。所传递的参数依次对应于arguments数组中的arguments[0],arguments[1]等元素。

  • 而按引用传递,则相当于将形式参数初始化为实际参数本身。

注意:一个是只传递变量的值,一个是传递整个变量。这两者的内涵是完全不同的。

我们知道,ECMAScript中,每个变量都是一个用于保存值的占位符。如果参数是对像,则参数按值传递,意味着在函数内部,只能操作实际参数(对像)的属性和值,而不能操作实际参数(对像)本身。也就是说,你可以通过函数来改变函数外部所传入对像(参数)的属性和属性值;但你不能删除该变量,或改变此变量的引用对象。

如果是按引用传递,则示例函数执行过程应该如下:

function setName(obj) {
    obj.name="Nicholas";
    obj=new Object();
    obj.name="Greg";
}
var person=new Object();
setName(person);

//将参数person传入setName函数;
function setName(person){ //将person对像本身传入函数
    peson.name="Nicholas"; 
    person=new Object( ) //重新定义对像的引用
    person.name="Greg";
}

alert(person.name); //"Greg"

得到的结果就不同了。

所以在对像参数传递时,最重要的是要理解,所谓的按值传递,意味着函数只能操作对像的属性和值,而不能操作对像本身。

原文链接:http://www.jianshu.com/p/26ab38bdc176

你可能感兴趣的:(JS中函数的参数是按值传递还是按引用传递?)