环境

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

栈内存

栈内存是用来执行代码和存储基本类型值的(创建的变量也存栈里面了)

  • 不仅全局代码执行(EC(G)全局执行上下文),
  • 而且函数执行(EC(X)私有上下文),最后也都会进栈执行的
  • 基于ES6中的let/const形成的块作用域也是栈内存
  • 的特点:
    • 后进先出,最后添加进栈的元素最先出。
    • 访问栈底元素,必须拿掉它上面的元素。
    • 3.1 - 底层机制之堆栈内存 - 图1

栈内存中语句的执行

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

堆内存Heap

我们所说的 数据结构指的是 二叉堆

  • 堆内存是用来存储引用数据类型值的
    • (例如:创建函数和创建对象,就是开辟一个堆内存,把代码字符串或者键值对存储到堆内存中的)

基本数据类型的声明和定义过程

  1. 首先声明一个变量,将这个【变量】存储到【当前栈内存】的【变量存储区】当中
  2. 创建一个值,将这个【值】存储到【当前栈内存】的【值存储区】当中
    • 注意:只有简单的基本数据类型是这样存储的,【复杂的引用数据类型的【值】】不是这样存储的。
  3. ‘=’ 赋值符号赋值的过程称为【定义】,实际上就是让【变量和值相关联的过程】。
  1. let a = 12;
  2. //声明一个变量a
  3. //创建一个值12
  4. //定义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. ==对象[数字索引] :仅数组可用=
  • ‘字符串’ 值->代表属性值本身
  • 字符串 对象->代表该字符串变量所存储的值
    1. //定义一个变量name,值为10
    2. var name = 10,
    3. gender = '性别';
    4. //定义一个对象obj
    5. var obj = {
    6. name:'zhangsan',
    7. 性别:'male'
    8. };
    9. //读取obj对象的属性值
    10. console.log(obj.name);//=>'zhangsan'
    11. console.log(obj['name']);//=>'zhangsan'
    12. //读取obj对象的属性值,obj[name]代表着要读取【name变量的值】作为【obj对象的属性名】所对应的【属性值】
    13. console.log(obj[name]);//此时的name是作为一个变量存在,代表其存储的值=>obj[10]=>undefined
    14. console.log(obj[gender]);//=>obj['性别']=>'male'

深拷贝与浅拷贝

引用数据类型在复制时,改了其中一个数据的值,另一个数据的值也会跟着改变,这种拷贝方式我们称为浅拷贝

在实际开发中,我们希望引用类型复制到新的变量后,二者是独立的,不会因为一个的改变而影响到另一个。这种拷贝方式就称为深拷贝

深拷贝,实际上就是重新在堆内存中开辟一块新的空间,把原对象的数据拷贝到这个新地址空间里来,通常来说,我们有两种方法:

  • 转一遍JSON再转回来 ,但是这个办法有一个问题,这只能转化一般常见数据,function,undefined等类型都无法通过这种变回来
  • 手动去写循环遍历

我们来看下第一种方法,代码如下所示:

  1. const data = { name: "大白" };
  2. const obj = JSON.parse(JSON.stringify(data));
  3. obj.age = 20;
  4. console.log("data = ", data);
  5. console.log("obj = ", obj);

运行结果如下:

3.1 - 底层机制之堆栈内存 - 图2

最后,我们来看下第二种写法,代码如下所示:

  1. const data = [{ name: "大白" }];
  2. let obj = data.map(item => item);
  3. obj.push({ name: "神奇的程序员" });
  4. console.log("data = ", data);
  5. console.log("obj = ", obj);

运行结果如下:

3.1 - 底层机制之堆栈内存 - 图3

练习

  1. let n = [10,20];//创建变量n,创建堆内存1,相关联
  2. //此时堆内存1[10,20]
  3. let m = n;//创建变量m,与堆内存1相关联
  4. let x = m;//创建变量x,与堆内存1相关联
  5. m[0] = 100;//修改变量m对应的堆内存1中的第一个元素
  6. //此时堆内存1[100,20]
  7. x = [30,40];//创建新的堆内存2,并与变量x相关联
  8. //此时堆内存1[100,20]
  9. //此时堆内存2[30,40]
  10. x[0] = 200;//修改变量x对应的堆内存2中的第一个元素
  11. //此时堆内存2[200,40]
  12. m = x;//将变量x对应的堆内存2的地址与m相关联
  13. m[1] = 300;//修改m对应的堆内存2的元素2
  14. //此时堆内存2[200,300]
  15. n[2] = 400;//修改变量n对应的堆内存1,添加新元素3
  16. //此时堆内存1[100,20,400]
  17. //此时堆内存2[200,300]
  18. console.log(n,m,x);
  19. //[100,20,400]
  20. //[200,300]
  21. //[200,300]

3.1 - 底层机制之堆栈内存 - 图4


面试题

  • 原题
  1. let a = {n:1};
  2. let b = a;
  3. a.x = a = {n:2};
  4. console.log(a.x);
  5. console.log(b);
  6. 复制代码
  • 解析
    • 如let a = b = 12 ;
      • 从右向左
      1. 创建一个值12
      2. b = 12
      3. let a = 12
    • 运算符优先级
      • a.x = a = {};
      • 因为成员访问(a.x)的优先级高于赋值运算符 ,运算时会先执行a.x
  1. let a = {n:1};
  2. //创建变量a,创建堆内存1(地址为0X1)
  3. //此时堆内存1中{n:1}
  4. let b = a;
  5. //创建变量b,与堆内存1相关联
  6. a.x = a = {n:2};
  7. //【像此类连等的,就是先左边与最右边关联,再往右与最右边关联】
  8. //1.创建堆内存2,当中存储{n:2}(地址为0X2)
  9. //2.在a相关联的堆内存1中,存储新的元素x,属性值为堆内存2的地址
  10. //3.将a与堆内存2相关联
  11. //此时,a与堆内存2关联,b与堆内存1相关联
  12. //堆内存1中{n:1,x:0X2}
  13. //堆内存2中{n:2}
  14. console.log(a.x);
  15. //输出堆内存2中的属性名为x的属性值
  16. //【堆内存2中没有x的属性值】=>输出undefined
  17. console.log(b);
  18. //输出堆内存1中的元素{n:1,{n:2}}
  19. console.log(b.x.n);
  20. //输出2
  21. console.log(b.n);
  22. //输出1
  23. 复制代码
  • 关键点
    • a.x = a = {n:2};
    • 【像此类连等的,就是先左边与最右边关联,再往右与最右边关联】
  1. 创建堆内存2,当中存储{n:2}(地址为0X2)
  2. 在a相关联的堆内存1中,存储新的元素x,属性值为堆内存2的地址
  3. 将a与堆内存2相关联

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

  • 原题

    1. let a = {n:1};
    2. let b = a;
    3. a.x = b;
  • 解析

    1. let a = {n:1};
    2. //创建变量a,创建堆内存1(地址为0X1)
    3. //此时堆内存1中{n:1}
    4. let b = a;
    5. //创建变量b,与堆内存1相关联
    6. a.x = b;
    7. //将b所关联的堆内存1的地址,作为属性名x的属性值,存储到堆内存1中。
    8. //每一个x中存的都是该堆内存的地址
    9. //结果{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
  1. let sy = Symbol('AA');
  2. let x = {
  3. 0:0
  4. };
  5. let obj = {
  6. 0:12,
  7. true:'xxx',
  8. null:20,
  9. sy1:100,
  10. [sy]:'zhangsan',
  11. [x]:10
  12. };
  13. console.log(obj["Symbol('AA')"]);//=>undefined
  14. //直接用字符串来获取获取不到,只能通过sy(代表Symbol('AA'))
  15. console.log(obj[sy]); //=> 'zhangsan';//=>Symbol('AA'):'zhangsan'//【相当于把sy变量储存的值作为属性名,来获取对象中该属性名对应的属性值】
  16. console.log(obj[x]); //=>obj['[object object]']=> 10;//引用类型值作为属性名,最后也是把对象转换为字符串作为属性名来处理的'[object object]'
  17. console.log(obj['sy1']); //=> 100//如果加上‘’引号,则意为获取属性名为x的属性值=>100
  18. //
  19. //
  20. //注意,如果设置为引用数据类型,最后也是把对象转换为字符串作为属性名来处理的'[object object]'
  21. //↓
  22. obj[x] = 100;//相当于obj['[object object]']=100
  23. //得到结果是将属性[object object]的值设置为:100
  24. //
  25. //
  26. //
  27. for(let key in obj){
  28. //for in遍历中获取到的属性名,读取其类型都会变为字符串
  29. //且Symbol类型的属性名将无法被(迭代)获取到
  30. console.log(key,typeof key)
  31. }
  32. /* 0 string
  33. true string
  34. null string
  35. sy1 string
  36. [object Object] string*/

三个例子:

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