环境
- 浏览器想要运行JS代码,一个是【执行者】(cpu分配线程来执行),另一个是执行所需的【空间】。
- 执行者:cpu分配一个主线程来自上而下的执行栈中的JS代码。
- 空间:从电脑的内存当中分配一块内存给程序,用来执行代码(称为【当前】程序的栈内存=>Stack)
栈内存
栈内存是用来执行代码和存储基本类型值的(创建的变量也存栈里面了)
- 不仅全局代码执行(EC(G)全局执行上下文),
- 而且函数执行(EC(X)私有上下文),最后也都会进栈执行的
- 基于ES6中的let/const形成的块作用域也是栈内存
- 栈的特点:
- 后进先出,最后添加进栈的元素最先出。
- 访问栈底元素,必须拿掉它上面的元素。
栈内存中语句的执行
- 线程在栈内存当中自上而下执行语句,而语句进入栈内存执行称为【进栈执行】,当前语句进栈执行完毕之后必须要【出栈】下一条语句才能继续的进栈执行。
堆内存Heap
我们所说的 堆 数据结构指的是 二叉堆
- 堆内存是用来存储引用数据类型值的
- (例如:创建函数和创建对象,就是开辟一个堆内存,把代码字符串或者键值对存储到堆内存中的)
基本数据类型的声明和定义过程
- 首先声明一个变量,将这个【变量】存储到【当前栈内存】的【变量存储区】当中
- 创建一个值,将这个【值】存储到【当前栈内存】的【值存储区】当中
- 注意:只有简单的基本数据类型是这样存储的,【复杂的引用数据类型的【值】】不是这样存储的。
- ‘=’ 赋值符号赋值的过程称为【定义】,实际上就是让【变量和值相关联的过程】。
let a = 12;
//声明一个变量a
//创建一个值12
//定义a为12(定义就是为变量赋值的过程)
引用数据类型的声明和定义过程
- 首先声明一个变量,将这个【变量】存储到【当前栈内存】的【变量存储区】当中
- 除了【当前栈内存】,内存会新分配一块内存(分配出的内存都会有一个十六进制的地址),称为【堆内存Heap】用来存储【引用数据类型】的【值】
- 把对象当中的键值对(属性名:属性值)依次存储到【为此对象分配的堆内存】中,并将【该堆内存的地址】存储到【当前栈内存的值存储区】当中
- 在当前栈内存当中,把【值存储区】中的【堆内存地址】赋值给【变量】,使之相关联。
注意:一旦创建新的引用类型的【值】(如a={n:1}、b=[10,20]…),都是要创建新的堆内存!!!!!【结合后面练习题理解一下】
区别
- 基本数据类型:按值操作(直接操作的是值),所以也叫【值类型】
- 引用数据类型:按引用地址操作(直接操作的是地址),操作的是堆内存的地址,所以叫【引用类型】
变量和属性名的区别
- 一个对象的属性名只有两种格式:【字符串和数字】(等基本类型,但其他的基本不用)
- 获取一个对象的某个属性值可以用:
- 对象.字符串属性名 【.点只支持字符串属性名】
- 对象[‘字符串属性名’]
- 对象[字符串]:此时的字符串是作为一个变量存在,代表其存储的值\color{red}{对象[字符串]: 此时的字符串是作为一个变量存在,代表其存储的值}对象[字符串]:此时的字符串是作为一个变量存在,代表其存储的值
- {属性名} 【在ES6中,如果属性名和属性值一样,如obj{name:name,age:12}(后面这个是变量),则可以简写成obj{name,age:12}】
- 对象[数字属性名]
- 对象[‘数字属性名’]
- ==对象[数字索引] :仅数组可用=
- ‘字符串’ 值->代表属性值本身
- 字符串 对象->代表该字符串变量所存储的值
//定义一个变量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'
深拷贝与浅拷贝
引用数据类型在复制时,改了其中一个数据的值,另一个数据的值也会跟着改变,这种拷贝方式我们称为浅拷贝。
在实际开发中,我们希望引用类型复制到新的变量后,二者是独立的,不会因为一个的改变而影响到另一个。这种拷贝方式就称为深拷贝。
深拷贝,实际上就是重新在堆内存中开辟一块新的空间,把原对象的数据拷贝到这个新地址空间里来,通常来说,我们有两种方法:
- 转一遍JSON再转回来 ,但是这个办法有一个问题,这只能转化一般常见数据,function,undefined等类型都无法通过这种变回来
- 手动去写循环遍历
我们来看下第一种方法,代码如下所示:
const data = { name: "大白" };
const obj = JSON.parse(JSON.stringify(data));
obj.age = 20;
console.log("data = ", data);
console.log("obj = ", obj);
运行结果如下:
最后,我们来看下第二种写法,代码如下所示:
const data = [{ name: "大白" }];
let obj = data.map(item => item);
obj.push({ name: "神奇的程序员" });
console.log("data = ", data);
console.log("obj = ", obj);
运行结果如下:
练习
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 ;
- 从右向左
- 创建一个值12
- b = 12
- let a = 12
- 运算符优先级
- a.x = a = {};
- 因为成员访问(a.x)的优先级高于赋值运算符 ,运算时会先执行a.x
- 如let a = b = 12 ;
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};
- 【像此类连等的,就是先左边与最右边关联,再往右与最右边关联】
- 创建堆内存2,当中存储{n:2}(地址为0X2)
- 在a相关联的堆内存1中,存储新的元素x,属性值为堆内存2的地址
- 将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"}