ECMAScript 中变量的复制,以及函数的参数传递

原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:原始值引用值

  • 原始值

    • 是存储在 栈(stack) 中的简单数据段,它们的值直接存储在变量访问的位置。
    • 包含 6 种:Undefined、Null、Boolean、Number、String 和 Symbol。
    • 存储在栈中的原因:因为这类原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域--栈中。这样存储便于迅速查寻变量的值
  • 引用值

    • 是存储在 堆(heap) 中的对象,也就是说,存储在变量处的值是一个指针,指向存储对象的内存处。
    • 存储在堆中的原因:引用值的大小会改变(如对象的属性等),所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在栈空间中的值是该对象存储在堆中的地址,而地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。
原始值 引用值
存储位置 栈(stack) 堆(heap)
存储内容 简单数据段 对象的地址

image.png

复制值

  • JavaScript 不允许直接访问内存位置,也就是不允许直接访问保存在堆内存中的对象。
  • 所以在访问一个对象时,实际上操作的是对该对象的引用(对象在堆内存中的地址),然后再按照这个地址去获得这个对象中的值,这就是 按引用访问
  • 原始类型的值(栈内存中的值)是可以直接访问到的。

原始值的复制

在通过变量把一个原始值赋值到另一个变量时,原始值会被直接复制到新变量的位置。这两个变量可以独立使用,互不干扰。

let num1 = 5;
let num2 = num1;
复制代码

image.png

在这个例子中,a 和 b 这两个变量是完全独立的,只是拥有相同的值而已,因为 b 的值是 a 的值的副本。

引用值的复制

  • 在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。
  • 操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来。
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name);  // "Nicholas"
复制代码

image.png

在这个例子中,根据定义‘存储在变量中的值也会被复制到新变量所在的位置’,这里复制的值其实是 obj1 的指针,也就是对象的地址 ‘0x1234’,这样 obj1 和 obj2 两个变量都指向堆内存中的同一个对象。

传递参数

牢记: ESMAScript 中所有函数的参数都是按值传递的!!!

如何记忆:函数参数的按值传递,与复制值的机制相同。即,原始值直接复制该值,引用值复制对象的地址

这里使用“红宝书”中经典的三个例子来说明究竟如何记忆函数的参数传递。

原始值作为函数的入参

example 1:

function addTen(num) { // num 是一个局部变量
    num += 10;
    return num;
}

let count = 20;
let result = addTen(count);
console.log(count);    // 20,没有变化
console.log(result);   // 30
复制代码

在这个例子中:

  • 函数 addTen() 有一个参数 num,它其实是一个局部变量,函数执行完之后就会销毁。
  • 在调用时,函数的入参是 count,是一个原始值
  • 由于函数的参数是 “按值传递” 的,因此将原始值复制,传入函数。
  • 接下来,遵从原始值的复制机制,将 count 的值复制一份,传入函数内部,赋值给函数作用域内的变量 num
  • 参数 num 和变量 count 互不干扰,它们只不过碰巧保存了一样的值。

ECMAScript 中函数的参数就是局部变量。

image.png

引用值作为函数的入参

example 2:

function setName(obj) {
    obj.name = "Nicholas";
}

let person = new Object();
setName(person);
console.log(person.name);    // "Nicholas"
复制代码

在这个例子中:

  • 在调用时,函数的入参是 person,是一个引用值(对象)
  • 因此将引用值复制,传入函数。
  • 接下来,遵从引用值的复制机制,将 person 的值复制一份,传入函数内部,这时传入函数局部变量 obj 的值就是 person 的地址

image.png

example 3:

function setName(obj) {
    obj.name = "Nicholas";
    obj = new Object();   // 这两句要注意
    obj.name = "Greg";    // 这两句要注意
}

let person = new Object();
setName(person);
console.log(person.name);    // "Nicholas"
复制代码

在这个例子中:

  • 在调用时,执行到 obj = new Object() 之前的所有步骤都没有变化,传入函数的参数是 person 对象在堆内存中的地址。
  • 执行 obj = new Object() 之后,在堆内存中重新开辟了一段内存,将其地址赋给了 obj,因此 obj 的值改变了,不再是 person 的地址值,改为指向新的内存地址。而 person 的值没有改变,故指向也没有改变。

image.png

image.png

总结

时刻牢记:

  • 变量存储在 栈内存 中;
  • 对象存储在 堆内存 中;
  • 变量中存储的值的类型,一种是原始值,一种是对象的引用(对象的地址)
  • 函数的参数传递,遵从 按值传递 原则,可以记忆为传递的是 栈内存 中的值,要么是一个原始值,要么是一个地址。

参考文献

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享