【JS红宝书¹⁷】关于初始值与引用值

176 阅读5分钟

前言

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

好久没更新学习笔记了,趁这段时间放假有空,抓紧学习一下

栈内存与堆内存

在讲初始引用值之前,我们先来看一下JavaScript中的内存空间

栈内存(stack):存放基本数据类型

基本数据类型保存在栈内存中,因为基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据;

特点: 栈内存中变量一般在它的当前执行环境结束就会被销毁被垃圾回收制回收

堆(heap):存放复杂数据类型

引用数据类型存储在堆内存中,因为引用数据类型占据空间大、大小不固定;

特点: 堆内存中的变量则不会,因为不确定其他的地方是不是还有一些对它的引用,只有当所有指向堆内存的指针全部销毁之后才会被垃圾回收

池(常量池):存放常量- 基本数据类型(一般把它归类到栈内存中)

原始值与引用值

以下是对于JavaScript中原始值与引用值的一个解释:

原始值(primitive value)[栈]:Undefined、Null、Boolean、Number、String 和 Symbol

保存原始值的变量是按 值(by value)访问的,操作的就是存储在变量中的实际值(实际存储空间)

引用值(reference value)[堆]:Array,Object,Function...

保存引用值的变量是按 引用(by reference)访问的,因为引用值是保存在内存中的对象,在JavaScript中操作的是该对象的引用(reference)而非实际的对象本身(指针引用)

动态属性

引用值:对于引用值而言可以随时添加、修改和删除其属性和方法

原始值:不能有属性,尽管尝试给原始值添加属性不会报错(但是无法访问)

例如:
    let str = "Hello World"
    str.name = "shrimpsss"
   
    // 但是无法访问!
    str // Hello World
    str.name // undefined

注意:原始类型的初始化可以只使用原始字面量形式;如果使用的是 new 关键字,则 JavaScript 会创建一个 Object 类型的实例,但其行为类似原始值

    let str = "Hello World"
    // 若想在后面执行 . 操作来调用内置方法是可行的
    str.splic(...) // ✅
    
    // 对象类型初始化
    let str = new String("Hello World");
    typeof str // 'object'
    str instaceof object //  true
     

如初始化的 new String("xx") 是可以挂在动态属性的,因为其类型为object而不是string

复制值

除了存储方式不同,原始值和引用值在通过变量复制时也有所不同

原始值的复制方式

通过变量把一个原始值赋值到另一个变量,原始值会被复制到新变量的位置,两者互不干涉

image-20220527141949772

我们直接看图就明白了,在上图,定义了 a1a2 两个变量,a2复制a1的值,a1的值跟存储在 a2 中的值是完全独立的,因为它是那个值的副本

引用值的复制方式

但是引用值就不一样了,虽然也会把值复制到新变量所在的位置,但是这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。

 let obj1 = {"name":"张三"};
 let obj2 = obj1;
 console.log(obj1,obj2); // { name: '张三' } { name: '张三' }
 obj2.name = "李四";
 console.log(obj1,obj2); // { name: '李四' } { name: '李四' }

在代码中可以看见,声明了变量名为 obj1 的对象引用,其次声明obj2将其指向obj1所存储的堆内存空间,打印他们输出的属性name的值都为张三,因为他们指向同一个内存空间;再则将obj2的name属性的值改为李四,相对于直接更改指向的堆内存中的值,所以obj2obj1的name属性的值理所应当都为李四

看图理解

image-20220527142938682

传递参数

原文:ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样;如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样

首先我们来看函数传递原始值参数

 let num = 10;
 function Test(num) {
     num += 10;
     return num;
 }
 let result = Test(num);
 console.log(num, result); // 10 20

解析:首先从外部把值为10的num 传递给Test函数作为参数,函数内部的num实际为局部变量;在函数内部将 num+10并返回num ,再将Test函数保存结果,最后打印结果外部num为10,内部num为20;

其实这一点很容易理解,函数是按值传参的,并不是按引用传参,内部参数与外部变量互不干涉;

函数传递引用值参数:

 let test = new Object();
 objTest(test);
 function objTest(obj) {
     obj.name = "张三";
     obj = new Object();
     obj.name = "李四";
     obj.hobby = "篮球";
 }
 console.log(test.name); // 张三
 console.log(test.hobby); // undefined

解析:开局 new了一个test对象,并将其传递给objTest作为实参,形参 obj即为test对象;函数内部首先在obj上挂在了name属性,值为张三;其次再将形参 obj 设置为一个新对象且 name 属性被设置为李四;再添加一个hobby属性为篮球;最后查看test的name还是为张三,而hobby却找不到;

从结果得出,即使对象是按值传进函数的,obj 也会通过引用访问对象;当函数内部给 obj 设置了 name 属性时,函数外部的对象也会反映这个变化,因为 obj 指向的对象保存在全局作用域的堆内存上

这表明函数中参数的值改变之后,原始的引用仍然没变;当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针,会在函数执行结束时销毁

总结

  • 原始值按值访问,引用值按引用访问
  • 复制原始值是直接复制到当前变量位置
  • 复制引用值是将其指向原变量的引用地址
  • ECMAScript函数中的参数就是局部变量

最后如果本文对于本文有疑惑,还请指导勘正 (●'◡'●)