今天看到《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函数的参数都是按值传递的。基本类型值的传递是按值传递,引用类型值的传递也是按值传递,只不过这个值存放的是地址。