理解 Java Script 中参数的传递

449 阅读6分钟

1.原始值和引用值

在 JavaScript 中,变量中可以包含原始值 (primitive value) 和引用值这两种数据类型,函数的参数传递方式有两种:值传递和引用传递。所以要想弄清参数的具体传递方式需要知道原始值和引用值的划分。

1.1原始值与引用值

原始值就是最简单的数据类型:

  • 数字(Number):整数和浮点数,例如 13.14-5 等。
  • 字符串(String):一串文本,例如 "hello"'world' 等。
  • 字符串(String):一串文本,例如 "hello"'world' 等。
  • 空值(Null):表示空值或不存在的对象,只有一个值 null
  • 未定义(Undefined):表示未定义的值,只有一个值 undefined
  • 符号(Symbol):表示唯一的标识符,ES6 新增的数据类型。-

保存原始值的变量是按值访问的,在实际中我们操作的其实就是存储在变量中的实际值,这一点是很好理解的。

引用值则是保存在内存中的对象,不同于原始值,JS是不允许直接访问内存位置,所以就不能直接操纵对象所在的内存空间。在我们操作对象的时候,可以把对象名看作指针,指向其所在的内存位置,我们操作的时候,操作的就是对象的引用,而非对象本身。因此,保存引用值是按引用访问的。

classDiagram
堆内存 <|-- 变量对象
堆内存 : object
class 变量对象{
object
(Object类型)
}

1.2动态属性

原始值和引用值定义方式相似,都可以创建一个变量然后给其赋值,但是赋值之后的操作却大不相同。对引用值而言,可以随时对其属性进行增删改查等操作。

       let obj = new Object()
       obj.name = 'bboxer'
       console.log(obj.name) 
    //    结果为'bboxer'

这里创建了一个变量,将其保存在obj中,然后给这个对象添加了名为name的属性,赋值为'bboxer'。之后就可以访问到这个新属性,直到对象被销毁或者属性被显示删除。

但是原始值不能有属性,尽管赋予其属性不会报错,比如:


      let name = 'bboxer'
      name.age = 24
      console.log(name.age)
    //     结果为undefined

这里想给name定义一个age属性并赋值为24,但是这个属性并没有生效,所以只有引用值能在其后面动态添加属性。

2.复制值

2.1原始值复制

原始值和引用值除了存储方式不同,在变量复制时也会不同,在通过变量将一个原始值赋值给另一个变量时,原始值会被复制到新变量的位置。

let num1 = 10
    let num2 = num1
    num1 =20
    console.log(num1)
    // num1的结果为20
    console.log(num2)
    // num2的结果为10
    

这里定义了num1并赋值为10,当把num2初始化为num1时,num2也会得到10这个值,并且这个值的存储和num1是完全独立的,所以在把num1的值改为20后,num2的值不会受到影响。

classDiagram
class 复制前的变量对象{
num1          10
(Number类型)
}
classDiagram
class 复制后的变量对象{
num1       10
num2        10

(Number类型)
}

2.2引用值复制

把引用值从一个变量复制到另一个变量时,存储在变量中的值也会被复制到新变量所在的位置,不过要注意:变量中存储的值是引用地址,可以看作一个指针,指向真正存储数据的堆中的对象里。复制完成后,两个变量实际指向同一个堆内存中的对象,所以操作其中一个的时候,另一个也会收到影响。

let obj1={}
        let obj2=obj1
        obj2.name='bboxer'
        console.log(obj1.name)
        // 结果为'bboxer'

这里obj1保存了一个对象的实例,这个值被复制到obj2,此时obj1和obj2指向堆内存中同一个对象,因此给obj2添加这个属性时,obj1也可以访问这个属性。

classDiagram
堆内存 <|-- 复制后的变量对象1
堆内存 <|-- 复制后的变量对象2
堆内存 : Object

class 复制后的变量对象1{
obj1
(Object类型)
}
class 复制后的变量对象2{
obj2
(Object类型)
}

3.传递参数

(易混淆) Js中所有参数都是按值传递的。函数外的值会被复制到函数内部的参数中,如果是原始值,那就和原始值的变量复制一样,如果是引用值,那就和引用值变量的复制一样,这一块不容易理解,因为变量可以按值和引用访问,但是传参只能按值传递,笔者在后面会做出解释。

3.1原始值按值传递参数

笔者先举一个实例:

function add(num){
            num += 10
            return num
        }
        let count = 10
        let result = add(count)
        console.log(count)
        // 10 没有变化
        console.log(result)
        // 20

这里比较好理解,是标准的按值传递的结果,add()有一个参数num,它是一个局部变量,在调用函数时,count的值会被复制到函数内部的参数num以便在函数中使用,但是如之前所讲,它们之间是相互独立的,所以对num进行操作时并不会影响到外部的count。但是如果传递的是对象,就没有这么好理解了。

3.2引用值按值传递参数(易混淆)

这里再举一个实例:

 function re(obj){
            obj.name='bboxer'
        }
        let setName={}
        re(setName)
        console.log(setName.name)
        // 结果是'bboxer'

这里我们创建了一个对象保存在setName中,然后这个对象被传递给re()方法,并被复制在参数obj中。在函数内部,如之前所说setName和obj同时指向一个对象,结果会造成,即使对象是按值传进函数的,但是obj也会通过引用访问对象,对obj添加name属性时,外部的setName也会反应这个变化,因为obj指向的对象保存在全局作用域的堆内存上。这里会有一点难以理解,为了证明对象是按值传递进函数的,笔者再举一个例子证明:

 function re(obj){
            obj.name='bboxer'
            obj={}
            obj.name='bboxerbai'
        }
        let setName={}
        re(setName)
        console.log(setName.name)
        // 结果还是'bboxer'

这里就很显而易见了,如果对象是按引用传递的,那么将obj设置为一个新对象,并且属性name被设置为'bboxerbai'时,其指针应该指向name为'bboxerbai'的对象,那么输出的值应该会变为'bboxerbai',但是实际上并没有,输出的值仍然是'bboxer',这说明函数中的参数值被改变时,原始的引用值没有改变。在obj函数被重写时,他变成了指向本地对象的指针,而那个指针在函数执行完毕后就被销毁了。

3.3拓展

这里先写一段代码再进行解释:

  function sum(num1,num2){
            return num1+num2
        }
        console.log(sum(10,20))
        // 结果为30
        let newSum=sum
        console.log(newSum(10,20))
        // 结果为30
        sum = null
        console.log(sum(10,20))
        // 这里会报错,因为sum现在已经不是一个函数了
        console.log(newSum(10,20))
        // 这里结果还是30,并没有受sum影响

我们在这里定义了一个求和函数,然后又声明了一个变量newSum,并将其的值设为sum,此时sum和nemSum同时指向一个对象,但将sum设置为null后,就切断了它与函数的关联,注意这里是直接操作的变量,而不是变量属性,所以切断与函数的联系,但newSum仍然指向堆中的对象,所以没有被影响。