在 JavaScript 中,我们有函数以及传递给这些函数的参数。但是 JavaScript
对如何处理你传递的内容并不总是很清楚。当你开始进入面向对象开发的时候,你可能会发现自己为什么在函数中有时能访问到值,但有时无法访问到值。
传入基本数据类型例如字符串或数字时,参数是按值传入的。这意味着任何在函数中对该变量的更改与函数之外发生的任何事情无关。下面我们来看看下面这个例子:
var value = 1;
function foo(v) {
v = 2;
}
console.log(value); //1
foo(value);
console.log(value) // 1
很好理解,当传递 value
到函数 foo
中,相当于拷贝了一份 value
,假设拷贝的这份叫 _value
,函数中修改的都是 _value
的值,而不会影响原来的 value
值。
拷贝虽然很好理解,但是当值是一个复杂的数据结构的时候,拷贝就会产生性能上的问题。
所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。
var obj = {
value: 1
};
function foo(o) {
o.value = 2;
console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
这个时候很多小伙伴就很困惑了,《Javascript 高级程序设计》第三版中有这么一句话 ECMAScript中所有函数的参数都是按值传递的
但是从刚刚的例题中我们明显感觉到:就是按照引用传递的啊;首先我们都知道,js
中的类型分为值类型和引用类型,值类型保存在内存栈中,引用类型保存在内存堆中。
我们看下面的这段代码:
var a={
name:'大雄'
};
var b=a;
b.name='小熊';
console.log(a.name);
最后的输出结果是小熊
,这段代码的执行结果想必大家都知道,因为a
保存的是一个堆指针,b
赋值后指向了同一个堆内存:
所以说:值传递
可以理解为复制变量值,基本类型复制后俩个变量完全独立;引用类型复制的是引用(即指针),之后的任何一方改变都会映射到另一方。
下面我们看另一道题:
var a = [1,2,3]
function fn(b){
b=[4,5,6];
};
fn(a)
console.log(a);
按照我们刚才讲的,这道题是不是应该输出[4,5,6]
呢?
错了,答案还是[1,2,3]
,因为这时我们并没有改变指针的内容;而是开辟了一个新的堆空间;所以a,b 就不相互影响了:
结合图和代码,我们对程序的执行一目了然,相信大家都明白了。
所以说js中函数的参数都是按值传递的
我们先看一道例题:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
console.log(i);
只要你对 JS 中同步和异步代码的区别、变量作用域、闭包等概念有正确的理解,就知道正确答案是 5 -> 5,5,5,5,5
。
如果期望代码的输出变成:5 -> 0,1,2,3,4,且不使用let
的前提下,该怎么改造代码?
熟悉闭包的同学很快能给出下面的解决办法:
for (var i = 0; i < 5; i++) {
(function(j) { // j = i
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
console.log(i);
巧妙的利用IIFE(Immediately Invoked Function Expression:声明即执行的函数表达式)
来解决闭包造成的问题,确实是不错的思路,但是初学者可能并不觉得这样的代码很好懂,还有其他的解决方法吗?
答案是有,我们只需要对循环体稍做手脚,让负责输出的那段代码能拿到每次循环的 i
值即可。该怎么做呢?利用 JS
中基本类型的参数传递是按值传递的特征,不难改造出下面的代码:
var output = function (i) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i); // 这里传过去的 i 值被复制了
}
console.log(new Date, i);