前端系统化学习【JS篇】:(五)原始值数据类型和对象数据类型的区别(堆栈内存底层机制)

253 阅读10分钟

前言

  • 细阅此文章大概需要 30分钟\color{red}{30分钟}左右
  • 本篇中详细讲述\color{red}{详细讲述}了:
    1. 堆栈内存机制简述
    2. 原始值数据类型的声明和定义过程
    3. 对象数据类型的声明和定义过程
    4. 原始值数据类型和对象数据类型的区别
    5. 对象中作为属性名字符串和变量的区别
    6. 一些例题
    7. 关于对象数据类型中【属性名类型】的深入
    8. 关于对象数据类型中【属性名类型】的三种情况例题
  • 如果有任何问题都可以留言给我,我看到了就会回复,如果我解决不了也可以一起探讨、学习。如果认为有任何错误都还请您不吝赐教,帮我指正,在下万分感谢。希望今后能和大家共同学习、进步。
  • 下一篇会尽快更新,已经写好的文章也会在今后随着理解加深或者加入一些图解而断断续续的进行修改。
  • 如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!

原始值数据类型和对象数据类型的区别

【从堆栈内存的角度来理解】

  • 浏览器想要运行JS代码,(或者说电脑当中程序想要运行),有两个要素缺一不可,一个是执行所需的【执行者】(程序中的代码需要cpu分配线程来执行),另一个是执行所需的【空间】。
    1. 空间:从电脑的内存当中分配一块内存给程序,用来执行代码(称为【当前】程序的栈内存=>Stack)
    2. 执行者:cpu分配一个主线程来自上而下的执行栈中的JS代码。

栈内存的空间分配

  • 当内存分配一块空间给作为栈内存给浏览器,基本会分为:
    • 【变量存储空间】
    • 【值存储空间】
    • 【栈内存】
  • 【值存储空间】中只有【原始值数据类型的赋值】和【对象数据类型的堆内存地址】存储在此。

堆内存Heap

  • 用来存储对象数据类型的【值】,相当于对象数据类型的【值存储空间】
  • 【堆内存的内存地址】将存储在【栈内存的值存储空间】

栈内存中语句的执行

  • 线程在栈内存当中自上而下执行语句,而语句进入栈内存执行称为【进栈执行】,当前语句进栈执行完毕之后必须要【出栈】下一条语句才能继续的进栈执行。

原始值数据类型的声明和定义过程

  1. 首先声明一个变量,将这个【变量】存储到【当前栈内存】的【变量存储区】当中
  2. 创建一个值,将这个【值】存储到【当前栈内存】的【值存储区】当中
    • 注意:只有简单的原始值数据类型是这样存储的,【对象数据类型的【值】】不是这样存储的。
  3. '=' 赋值符号赋值的过程称为【定义】,实际上就是让【变量和值相关联的过程】。
    let a = 12;
    //声明一个变量a
    //创建一个值12
    //定义a为12(定义就是为变量赋值的过程)

对象数据类型的声明和定义过程

  1. 首先声明一个变量,将这个【变量】存储到【当前栈内存】的【变量存储区】当中
  2. 除了【当前栈内存】,内存会新分配一块内存(分配出的内存都会有一个十六进制的地址),称为【堆内存Heap】用来存储【对象数据类型】的【值】
  3. 把对象当中的键值对(属性名:属性值)依次存储到【为此对象分配的堆内存】中,并将【该堆内存的地址】存储到【当前栈内存的值存储区】当中
  4. 在当前栈内存当中,把【值存储区】中的【堆内存地址】赋值给【变量】,使之相关联。

注意:一旦创建新的引用类型的【值】(如a={n:1}、b=[10,20]...),都是要创建新的堆内存!!!!!【结合后面练习题理解一下】


区别

  • 原始值数据类型:按值操作(直接操作的是值),所以也叫【值类型】
  • 对象数据类型:按引用地址操作(直接操作的是地址),操作的是堆内存的地址,所以叫【引用类型】

变量和属性名的区别

  • 一个对象的属性名只有两种格式:【字符串和数字】(等基本类型,但其他的基本不用)
  • 获取一个对象的某个属性值可以用:
    1. 对象.字符串属性名 【.点只支持字符串属性名】
    2. 对象['字符串属性名']
    3. 对象[字符串]:此时的字符串是作为一个变量存在,代表其存储的值\color{red}{对象[字符串]: 此时的字符串是作为一个变量存在,代表其存储的值}
      • {属性名} 【在ES6中,如果属性名和属性值一样,如obj{name:name,age:12}(后面这个是变量),则可以简写成obj{name,age:12}】
    4. 对象[数字属性名]
    5. 对象['数字属性名']
    6. ========对象[数字索引] :仅数组可用=======
  • '字符串' 值---->代表属性值本身

  • 字符串 对象---->代表该字符串变量所存储的值

    //定义一个变量name,值为10
    var name = 10,
    	gender = '性别';
    //定义一个对象obj
    var obj = {
        name:'zhangsan',
        性别:'male'
    };
    //读取obj对象的属性值
    console.log(obj.name);//=>'zhangsan'
    console.log(obj['name']);//=>'zhangsan'
    //读取obj对象的属性值,obj[name]代表着要读取【name变量的值】作为【obj对象的属性名】所对应的【属性值】
    console.log(obj[name]);//此时的name是作为一个变量存在,代表其存储的值=>obj[10]=>undefined
    console.log(obj[gender]);//=>obj['性别']=>'male'
    

练习


    let n = [10,20];//创建变量n,创建堆内存1,相关联
    //此时堆内存1[10,20]
    let m = n;//创建变量m,与堆内存1相关联
    let x = m;//创建变量x,与堆内存1相关联
    m[0] = 100;//修改变量m对应的堆内存1中的第一个元素
    //此时堆内存1[100,20]
    x = [30,40];//创建新的堆内存2,并与变量x相关联
    //此时堆内存1[100,20]
    //此时堆内存2[30,40]
    x[0] = 200;//修改变量x对应的堆内存2中的第一个元素
    //此时堆内存2[200,40]
    m = x;//将变量x对应的堆内存2的地址与m相关联
    m[1] = 300;//修改m对应的堆内存2的元素2
    //此时堆内存2[200,300]
    n[2] = 400;//修改变量n对应的堆内存1,添加新元素3
    //此时堆内存1[100,20,400]
    //此时堆内存2[200,300]
    console.log(n,m,x);
    //[100,20,400]
    //[200,300]
    //[200,300]


面试题

  • 原题
        let a = {n:1};
        let b = a; 
        a.x = a = {n:2};
        console.log(a.x);
        console.log(b);
  • 解析
    • 如let a = b = 12 ;
      • 从右向左
      1. 创建一个值12
      2. b = 12
      3. let a = 12
    • 运算符优先级
      • a.x = a = {};
      • 因为成员访问(a.x)的优先级高于赋值运算符 ,运算时会先执行a.x

        let a = {n:1};
        //创建变量a,创建堆内存1(地址为0X1)
        //此时堆内存1中{n:1}
        let b = a; 
        //创建变量b,与堆内存1相关联
        a.x = a = {n:2};
        //【像此类连等的,就是先左边与最右边关联,再往右与最右边关联】
        //1.创建堆内存2,当中存储{n:2}(地址为0X2)
        //2.在a相关联的堆内存1中,存储新的元素x,属性值为堆内存2的地址
        //3.将a与堆内存2相关联

        //此时,a与堆内存2关联,b与堆内存1相关联
        //堆内存1中{n:1,x:0X2}
        //堆内存2中{n:2}
        console.log(a.x);
        //输出堆内存2中的属性名为x的属性值
        //【堆内存2中没有x的属性值】=>输出undefined
        console.log(b);
        //输出堆内存1中的元素{n:1,{n:2}}
        console.log(b.x.n);
        //输出2
        console.log(b.n);
        //输出1
  • 关键点
    • a.x = a = {n:2};
    • 【像此类连等的,就是先左边与最右边关联,再往右与最右边关联】
  1. 创建堆内存2,当中存储{n:2}(地址为0X2)
  2. 在a相关联的堆内存1中,存储新的元素x,属性值为堆内存2的地址
  3. 将a与堆内存2相关联

练习题2(是错误用法,形成了堆的嵌套,导致内存无限溢出)

  • 原题
        let a = {n:1};
        let b = a;
        a.x = b;
    
  • 解析
        let a = {n:1};
         //创建变量a,创建堆内存1(地址为0X1)
            //此时堆内存1中{n:1}
        let b = a;
         //创建变量b,与堆内存1相关联
        a.x = b;
         //将b所关联的堆内存1的地址,作为属性名x的属性值,存储到堆内存1中。
         //每一个x中存的都是该堆内存的地址
         //结果{n:1,x:{{n:1,x:{{n:1,x:{{n:1,x:{{n:1,x:{......}}}}}}}}}}
    

关于对象数据类型中【属性名类型】的深入

  • 在对象数据类型当中,存在着零到多组的键值对(属性名和属性值)
    • 而关于属性名的类型有两种说法:
      • 【说法一:属性名类型只能是字符串或者Symbol】
      • 【说法二:属性名类型可以是任何 基本类型值 ,处理中可以和字符串互通】
      • 注意: 但 【属性名绝对不能是对象数据类型,如果设置为对象数据类型,最后也是把对象转换为字符串作为属性名来处理的'[object object]'】
    • 在forin循环中
      • for in遍历中获取到的属性名,typeof读取其类型都会变为字符串
      • 且Symbol类型的属性名将无法被(迭代)获取到
    • 关于obj[x]和obj['x']的区别:【变量和属性名的区别】
      • 首先属性名肯定得是一个值
      • obj[x]:相当于把x变量储存的值作为属性名,来获取对象中该属性名对应的属性值
      • obj['x'](obj.x):如果加上‘’引号,则意为获取属性名为x的属性值=>100
    let sy = Symbol('AA');
    let x = {
        0:0
    };
    let obj = {
        0:12,
        true:'xxx',
        null:20,
        sy1:100,
        [sy]:'zhangsan',
        [x]:10
    };
    console.log(obj["Symbol('AA')"]);//=>undefined
    //直接用字符串来获取获取不到,只能通过sy(代表Symbol('AA'))
    console.log(obj[sy]); //=> 'zhangsan';//=>Symbol('AA'):'zhangsan'//【相当于把sy变量储存的值作为属性名,来获取对象中该属性名对应的属性值】
    console.log(obj[x]); //=>obj['[object object]']=> 10;//引用类型值作为属性名,最后也是把对象转换为字符串作为属性名来处理的'[object object]'
    console.log(obj['sy1']); //=> 100//如果加上‘’引号,则意为获取属性名为x的属性值=>100
    //
    //
    //注意,如果设置为对象数据类型,最后也是把对象转换为字符串作为属性名来处理的'[object object]'
    //↓
    obj[x] = 100;//相当于obj['[object object]']=100
    //得到结果是将属性[object object]的值设置为:100
    //
    //
    //
    for(let key in obj){
        //for in遍历中获取到的属性名,读取其类型都会变为字符串
        //且Symbol类型的属性名将无法被(迭代)获取到
        console.log(key,typeof key)
    }
    /*  0 string
        true string
        null string
        sy1 string
        [object Object] string*/

三个例子:

  • 用变量作为属性名
       var a = {},
           b = '0',
           c = 0;
           //
           /* 因为都是用变量存储的值作为属性名,所以两个0都指同一个属性值 */
           //
       a[b] = 'zhang';//a['0'] = 'zhang'//指属性名为‘0’的属性值
       a[c] = 'san';//a[0] = 'san'//指属性名为‘0’的属性值
       console.log(a[b]);//=>'san
       console.log(a);//=>{0: "san"}
  • 用两个唯一值做属性名
       var a = {},
            b = Symbol('1'),
            c = Symbol('1');//两个唯一值不同,所以是两个不同的属性名
        a[b] = 'zhang';
        a[c] = 'san';
        console.log(a[b]);//=>'zhang'
        console.log(a);//=>{Symbol(1): "zhang", Symbol(1): "san"}
  • 用对象数据类型做属性名
       var a = {},
            b = {n:'1'},
            c = {m:'2'};
            //设置为对象数据类型,最后也是把对象转换为字符串作为属性名来处理的'[object object]'
        a[b] = 'zhang';//a['[object object]']='zhang'
        a[c] = 'san';//a['[object object]']='san'
        console.log(a[b]);//=>'san'
        console.log(a);//=>{[object Object]: "san"}