求值策略(js中参数是按值传递还是按引用传递)


  • 求值策略(js中参数是按值传递还是按引用传递)
  • 前言
  • 求值策略(evaluation strategy)
  • 1.按值传递(call by value)
  • 2.按引用传递(call by reference)
  • 3.按共享传递(copy of sharing)
  • 4.三者总结
  • ECMAscript实现
  • 1.声明变量时不同的内存分
  • 1.1原始值
  • 1.2引用值
  • 2.复制变量时不同
  • 2.1原始值
  • 2.2引用值
  • 3.向函数传递参数时不同
  • 3.1原始值
  • 3.2引用值
  • 特声明
  • 参考链接


前言

其实之前在看高程这本书的时候就看到过js传值的时候是按值传递还是按引用传递这一问题,按值传递理解起来很简单,但是按引用传递就有点模糊,并不是说原来多么难理解,而是当时根本就没有把按引用传递这个概念弄清楚,所以在了解任何问题之前,一定要先弄清楚概念和术语,再谈深入理解。
所以本篇文章是由高程一书的第四章节引发了一系列的血案:

java通过一个方法给传入参数赋值数组_按值传递

求值策略(evaluation strategy)

在计算机科学里面,有一部分叫求值策略。它决定变量之间,函数调用时实参和形参之间值是如何传递的。
需要多说一点的是,在求值理论里一般有两种求值策略:
严格–参数在键入程序之前是经过计算过的
非严格–参数计算是根据计算要求才去计算的
然后,这里我们考虑基本的函数传参策略,从ECMAScript出发点来说是非常重要的。首先需要注意的是,在ECMAScript中(甚至其他的语如,C,JAVA,Python和Ruby中)都使用了严格的参数传递策略。
另外传递参数的计算顺序也是很重要的——在ECMAScript是左到右,而且其它语言实现的反省顺序(从右向做)也是可以用的。
严格的传参策略也分为几种子策略,其中最重要的一些策略我们在本章详细讨论。
下面讨论的策略不是全部都用在ECMAScript中,所以在讨论这些策略的具体行为的时候,我们使用了伪代码来展示。

1.按值传递(call by value)

函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。一般来说,是重新分配了新内存(我们不关注分配内存是怎么实现的——也是是栈也许是动态内存分配),该新内存块的值是外部对象的拷贝,并且它的值是用到函数内部的。

bar = 10

procedure foo(barArg):
  barArg = 20;
end

foo(bar)

// foo内部改变值不会影响内部的bar的值
print(bar) // 10

但是,如果该函数的参数不是原始值而是复杂的结构对象是时候,将带来很大的性能问题,C++就有这个问题,将结构作为值传进函数的时候——就是完整的拷贝。
我们来给一个一般的例子,用下面的赋值策略来检验一下,想想一下一个函数接受2个参数,第1个参数是对象的值,第2个是个布尔型的标记,用来标记是否完全修改传入的对象(给对象重新赋值),还是只修改该对象的一些属性。

// 注:以下都是伪代码,不是JS实现
bar = {
  x: 10,
  y: 20
}

procedure foo(barArg, isFullChange):

  if isFullChange:
    barArg = {z: 1, q: 2}
    exit
  end

  barArg.x = 100
  barArg.y = 200

end

foo(bar)

// 按值传递,外部的对象不被改变
print(bar) // {x: 10, y: 20}

// 完全改变对象(赋新值)
foo(bar, true)

//也没有改变
print(bar) // {x: 10, y: 20}, 而不是{z: 1, q: 2}

2.按引用传递(call by reference)

函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值。简而言之,引用相当于是外部变量的别名,实际操作的就是该变量,即在函数内对该变量进行修改的话,在外部该变量也会相应被修改。

procedure foo(barArg, isFullChange):

  if isFullChange:
    barArg = {z: 1, q: 2}
    exit
  end

  barArg.x = 100
  barArg.y = 200

end

// 使用和上例相同的对象
bar = {
  x: 10,
  y: 20
}

// 按引用调用的结果如下: 
foo(bar)

// 对象的属性值已经被改变了
print(bar) // {x: 100, y: 200}

// 重新赋新值也影响到了该对象
foo(bar, true)

// 此刻该对象已经是一个新对象了
print(bar) // {z: 1, q: 2}

3.按共享传递(copy of sharing)

调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:函数内部给参数重新赋新值不会影响到外部的对象(和上例按引用传递的case),但是因为该参数是一个地址拷贝,所以在外面访问和里面访问的都是同一个对象(例如外部的该对象不是想按值传递一样完全的拷贝),改变该参数对象的属性值将会影响到外部的对象。

注意:这里出现的引用,我们不能称之为“按引用传递”,因为函数接收的参数不是直接的对象别名,而是该引用地址的拷贝。

procedure foo(barArg, isFullChange):

  if isFullChange:
    barArg = {z: 1, q: 2}
    exit
  end

  barArg.x = 100
  barArg.y = 200

end

//还是使用这个对象结构
bar = {
  x: 10,
  y: 20
}

// 按贡献传递会影响对象 
foo(bar)

// 对象的属性被修改了
print(bar) // {x: 100, y: 200}

// 重新赋值没有起作用
foo(bar, true)

// 依然是上面的值
print(bar) // {x: 100, y: 200}

4.三者总结

  • 按值传递:就是把值的拷贝传递进去,形参的改变不会影响实参。
  • 按引用传递:就是把地址(这个地址存在变量中,这个地址指向堆中的对象)传递进去,操作形参就是操作实参,
    对形参重新赋值,会影响实参。
  • 按共享传递:就是把地址的拷贝传递进去,实参和形参中的地址同时指向对象,所以形参对对象的属性的操作,通 过实参都能反映出来,但是对形参重新赋值,不会影响到实参。

ECMAscript实现

在ECMAscript中,变量可以存在两种类型,即原始值和引用值
原始值:

存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。

引用值:

存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存处。

1.声明变量时不同的内存分

1.1原始值

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

1.2引用值

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

java通过一个方法给传入参数赋值数组_java通过一个方法给传入参数赋值数组_02

2.复制变量时不同

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

2.1原始值

在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。

var num1=5;
var num2=num1;

java通过一个方法给传入参数赋值数组_引用传递_03

2.2引用值

在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。(这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)

var obj1=new Object();
var obj2=obj1;
obj1.name="lss"
alert(obj2.name)//lss

java通过一个方法给传入参数赋值数组_按值传递_04

3.向函数传递参数时不同

首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。但是为什么涉及到原始类型与引用类型的值时仍然有区别呢,还不就是因为内存分配时的差别。 (我对比了一下,这里和复制变量时遵循的机制完全一样的嘛,你可以简单地理解为传递参数的时候,就是把实参复制给形参的过程)

3.1原始值

按值传递的。

var x="实参";
function foo(x) {
    console.log(x); //实参
    x="这是形参,我在函数内部修改了这个值";
    console.log(x); //这是形参,我在函数内部修改了这个值
}
foo(x);
console.log(x);//实参

由于例子看出,在js中,函数内部修改了形参,并不会影响外面的实参,就是因为函数内部的形参只是一个副本。

3.2引用值

在js中,引用值是按共享传值的。

var obj={
    name:"我是obj"
};
function foo(obj) {
    console.log(obj);//{name: "我是obj"}
    obj.name="我要在函数内部改变";
    obj.info="我要在函数内部给obj添加一个属性";
    console.log(obj);//{name: "我要在函数内部改变", info: "我要在函数内部给obj添加一个属性"}

}
foo(obj);
console.log(obj);//{name: "我要在函数内部改变", info: "我要在函数内部给obj添加一个属性"}

看了这段代码,你内心肯定想,c,这不就是按引用传递嘛,根本不是书上所说的按值传递啊,因为改变函数内部的对象,外部的对象也给变了。其实不然,千万别被假象所迷惑,天真的孩子们啊,如果js中引用类型真的是按引用传值的话,那么,在函数内部给对象重新赋值,那么函数外部的对象也会被重新赋值,因为按引用传值,实际上就是操作在外部定义的obj这个对象本身。让我们来看一看在js中是不是这样的。

var obj={
    name:"我是obj"
};
function foo(obj) {
    console.log(obj);//{name: "我是obj"}
    //我要给函数内部的对象重新赋值
    obj={
        info:"我是新的值"
    };
    console.log(obj);//{info: "我是新的值"}

}
foo(obj);
console.log(obj);//{name: "我是obj"}

看见没有,在函数内给对象赋值了新值,函数外部的对象纹丝不动啊,分分钟举例就反驳了你的认知吧。所以在js中,引用类型是按共享传值的,传递给函数的只是对象在栈中保存的地址(这个地址指向在堆中存储的对象),原理就跟引用类型复制一样。
此外,如果只考虑ECMA-262标准所提供的抽象层次,我们在算法里看到的只有“值”这个概念,实现传递的“值”(可以是原始值,也可以是对象),但是按照我们上面的定义,也可以完全称之为“按值传递”,因为引用地址也是值。

特声明

本文是关于其他优秀博文的总结,也好供自己以后回顾和复习,理清其中概念,共勉之,有不足之处望各位小主指出,我定改之,臣退了。

参考链接



https://www.jb51.net/article/60568.htm
http://www.w3school.com.cn/js/pro_js_value.asp