阅读 1318

JavaScript参数传递的深入理解

今天看到《JavaScript高级程序设计》里面关于参数传递的章节时,有点懵。本着“打破砂锅问到底”的精神,看了些别人写的博客和知乎上一些大神的解释,算是对参数传递有了个比较全面的了解。

变量在内存中的存放方式

在讲参数传递前,先要理解变量在内存中的存放方式。ECMAScript变量有可能是5种基本类型的值(Undefined,Null,Boolean,Number,String),也有可能是引用类型值(Object),即对象。

对于值是5种基本类型的变量而言,变量是放在栈内存里的。因为这些变量占据的内存是固定的,这样存储便于迅速查寻变量的值。例如:

var name = "Nicholas",

    city = "Beijing",

   age = 22;

这些变量的存储结构为:


变量名和变量的值都放在栈内存。

而对于值是引用类型值的变量而言,是同时保存在栈内存和堆内存中的。例如:

var obj1 = {name:"Nicholas"},

    obj2 = {name:"Greg"};

这些变量的存储结构为:


在栈内存里没有直接存对象,而是存的对象在堆内存中的地址。对象的属性和方法都包含在对象里。

复制变量值

了解了变量在内存中的存储方式后,还要理解变量赋值的过程。用一个变量向另一个变量赋值时,基本类型值和引用类型值也会有所不同。如果用一个变量向新变量赋基本类型值,会在变量上创建一个新值,然后把该值赋给为新变量分配的位置上。例如:

var num1 = 5,

    num2 = num1,

    num1 = 10;

alert(num2); //5

当用变量num1为num2赋值时,num2中也保存了值5。但这个5与变量num1中的5是相互独立的,互不影响。即便后来num1的值变为10,num2的值还是5。

当从一个变量向另一个变量复制引用类型的值时,同样也会把存储在变量对象中值复制一份到新变量分配的空间中。前面提到过,这个值实际上是一个指针,而这个指针是指向存储在堆中的一个对象。由于这两个变量的值相同(即指针相同,指向同一个对象),所以改变一个变量的时,另一个变量也会改变。例如:

var obj1 = new Object(),

    obj2 = obj1;

   obj1.name = "Nicholas";

   alert(obj2.name);//"Nicholsa"

   obj2.name = "Greg";

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

首先是变量obj1保存了一个对象的新实例,当obj1的值赋给obj2时,实际上是把obj1指向这个对象的地址赋给了obj2,然后obj2也指向这个新对象。当为obj1添加属性后,obj2也能访问这个属性,并且属性值是相同的。因为这两个变量指向的是同一个对象。

但是如果为obj2赋值后,又新建一个对象实例赋值给obj2,那么obj2将不在指向obj1。obj1和obj2将相互独立,互不影响。例如:

var obj1 = new Object(),

    obj2 = obj1,

    obj2 = new Object();//新建一个对象实例,将在堆内存中重新分配地址空间

    obj1.name ="Nicholas";

    alert(obj2.name); //undefined

    obj2.name = "Greg";

    alert(obj1.name); //"Nicholas"

传递参数

讲完前面两点,可以进入正题了——JS中函数参数的传递方式。

函数参数传递的过程实际就是实参向形参复制值的过程。在向参数传递基本类型的值时,被传递(实参)的值会复制给一个局部变量(形参),形参值的变化不会对函数外的实参产生影响。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给形参。这时这个形参也指向了函数外的实参,因此这个形参的变化也会导致实参的变化。例如:

function addTen(num) { 

   num += 10;

   return num;

}

var  count = 20,

result = addTen(count);

alert(count);//20,形参值的变化不会影响实参

alert(result);//30

这里函数addTen()的参数num,实际上是函数的局部变量。在调用函数时,变量count作为参数传递给函数。由于count的值是20,所以数值20被复制给参数num。在函数内部,这个参数被加了10,但这并不会影响函数外部的count变量。

当向参数传递的值为对象时,例如:

function setName(obj) { 

obj.name = "Nicholas";

}

var person = new Object();

person.name = "Greg";

setName(person);

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

这里首先是创建了一个对象,保存在变量person中,并且给变量的name属性赋值为"Greg"。然后这个变量被当作参数传递给函数setName的参数obj。在函数内部,obj和person指向同一个对象,因为传递的是对象的地址。所以给obj的name属性赋值后,也会改变person的name属性值。但如果在函数内部为obj新建一个对象实例,这个新对象实例会开辟新的内存空间,导致obj的地址和person不同。此时,obj和person将指向两个不同的对象,所以互不影响。例如:

function setName(obj){

    obj.name = ""Nicholas;//这个obj和person指向的地址相同,即函数外person创建的对象。

    obj = new Object();//新建实例对象,导致obj指向另一个地址

    obj.name = "Greg";

}

var person = new Object();

person.name = "Jhon";

setName(person);

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

再举个例子:

var obj1 = { value:'111'};

var obj2 = { value:'222'};

function changeStuff(obj){ 

    obj.value = '333'; 

    obj = obj2; 

    return obj.value;

}

var foo = changeStuff(obj1);

console.log(foo);// '222' 参数obj指向了新的对象

console.log(obj1.value);//'333'

整个过程可以用下图表示:



总结:JavaScript函数的参数都是按值传递的。基本类型值的传递是按值传递,引用类型值的传递也是按值传递,只不过这个值存放的是地址。


评论