首先说一下这一系列基本上都是整理引用自大大这波能反杀的文章《前端基础进阶系列,中途会加上一些自己的扩展延伸,以便加深自己对底层js的理解。若有侵权请指正。
一、内存空间
JavaScript中并没有严格意义上区分栈内存与堆内存。因此我们可以简单粗暴的理解为JavaScript的所有数据都保存在堆内存中。但是在某些场景,我们仍然需要基于堆栈数据结构的思维来实现一些功能,因此理解栈数据结构的原理与特点十分重要。
三种数据结构:堆(heap),栈(stack)、队列(queue)。
堆数据结构:key-value,按值查找。就像图书馆按书名查。(如变量对象的存储)
栈数据结构:先进后出,后进先出,就像乒乓球盒子。(如执行上下文执行顺序,也就是函数调用栈)
队列:先进先出(FIFO)。就像排队过安检
队列图示
变量对象与基础数据类型
JavaScript的执行上下文生成之后,会创建一个叫做变量对象的特殊对象,JavaScript的基础数据类型往往都会保存在变量对象中。
变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其于堆内存区分开来。
基础数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6)。按值访问,这些类型可以直接操作保存在变量中的实际的。
基础类型使用声明式赋值,永远都是去常量池中查找Number、null 、undefined、String、Boolean,没有再次声明就放在常量池中,若声明的变量名已存在就使用同一个值,不重复分配地址。
//虽然字面就很好理解,但从堆内存存储的角度重新理解下
var a=1;
var a;
console.log(a);//1 同名即是同一个地址,直接能访问其中的值。
var b=2;
var b=3
console.log(b);//3 因为访问的是同一个变量地址,改变就能直接改变这个值
引用数据类型与堆内存
Object(在JS中除了基本数据类型以外的都是对象,数据是对象,函数是对象,正则表达式也是对象)
var a1 = 0; // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象
var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中,b为存储{m:20}对象的地址(指针)
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中
上例图解
// demo01
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?
console.log(a);//20
console.log(b);//30
// demo02
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
// 这时m.a的值是多少
console.log(m.a);//15
demo1的a是常量变量,b是另一个声明的变量,在执行阶段把a复制了一份给b,b就直接获得了a的值。而这两个变量的值相互独立互不影响。
而在demo02中,m变量存的指向堆内存中{a: 10, b: 20}的地址,因此var n = m执行的是一次复制引用类型的操作。同样也是为新的变量自动分配一个新的值保存在n中,不同的是,这个新的值只是引用类型的一个地址指针。尽管他们相互独立,但是在变量对象中访问到的具体对象实际上是同一个。如图所示。
demo02图解
基础数据类型的比较是值的比较
var a = 1;
var b = 1;
console.log(a === b);//true
引用类型的比较是引用的比较(堆的空间)
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
共享传递,深拷贝和浅拷贝
当数据作为实参传递函数的形参时,也是分数据类型的。
按值传递就是把实参在内存栈中的数据传递给形参, 然后在方法内部就可以使用形参的值了, 而引用传递(共享传递)是把实参的内存栈的地址编号传递给形参。
//demo3
var value = 1;
function foo(v) {
v = 2;
console.log(v); //2
}
foo(value);
console.log(value) // 1
demo3 foo(value)执行时,v得到value的拷贝,在内存中为形参v分配了一个地址, 其中保存了1这个值, 这时给v赋值,是直接修改形参v的值,而value的值不会变。
//demo4
var obj = {
value: 1
};
function foo(o) {
console.log(o);//{value:1}
o.value = 2;
console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
demo4中形参o拷贝得到是obj的指针地址,指向堆内存中的同一个对象,因此改变o的属性时,obj的属性也会改变。
//demo5
var obj = {
value: 1
};
function foo(o) {
o = 2;
console.log(o); //2
}
foo(obj);
console.log(obj) // {value: 1}
共享传递: 在传递对象的时候,拷贝了一份对象引用地址的副本,然后对这个引用进行操作。demo4也是共享传递(引用传递)
demo5直接改变了形参o的数据类型,让它不再指向obj所指向的地址,因此obj地址中的对象内容自然不会变。
浅拷贝
前面已经提到,在定义一个对象或数组时,变量存放的往往只是一个地址。当我们使用对象拷贝时,如果属性是对象或数组时,这时候我们传递的也只是一个地址。因此子对象在访问该属性时,会根据地址回溯到父对象指向的堆内存中,即父子对象发生了关联,两者的属性值会指向同一内存空间。
var a={key1:"11111"}
function Copy(p){
var c ={};
for (var i in p){
c[i]=p[i]
}
return c;
}
a.key2 = ["小辉","小辉"]
var b = Copy(a);
b.key3 = "33333"
alert(b.key1)//11111
alert(b.key3)//33333
alert(a.key3);//undefined
b.key2.push("大辉")
alert(a.key2);//小辉,小辉,大辉
本来b只是一个新的对象,return的c得到的形参p的每一个属性,和a指向的地址毫无关联,但是当修改的属性变为对象或数组时,那么父子对象之间就发生关联,原因是key1的值属于基本类型,所以拷贝的时候传递的就是该数据段;但是key2的值是堆内存中的对象,所以key2在拷贝的时候传递的是指向key2对象的地址,无论复制多少个key2,其值始终是指向父对象的key2对象的内存空间。
//ES6实现浅拷贝的方法
var a = {name:"暖风"} var b= Object.assign({},a);
b.age = 18;
console.log(a.age);//undefined
----------------------------------
//数组
var a = [1,2,3]; var b = a.slice();
b.push(4);
b//1,2,3,4
a//1,2,4
----------------------------------
var a = [1,2,3]; var b = a.concat();
b.push(4);
b//1,2,3,4
a//1,2,4
----------------------------------
var a = [1,2,3]; var b = [...a]
b//1,2,3,4
a//1,2,4</pre>
深拷贝
或许浅拷贝并不是我们在实际编码中想要的结果,我们不希望父子对象之间产生关联并且父对象的内容被改变,那么这时候可以用到深拷贝。既然属性值类型是数组和或象时只会传址,那么我们就用递归来解决这个问题,把父对象中所有属于对象的属性类型都遍历赋给子对象即可。测试代码如下:
var a={key1:"11111"}
function Copy(p,c){
var c =c||{};
for (var i in p){
if(typeof p[i]==="object"){
c[i]=(p[i].constructor ===Array)?[]:{}
Copy(p[i],c[i]);
}else{
c[i]=p[i]
}
}
return c;
}
a.key2 = ["小辉","小辉"]
var b = {}
b = Copy(a,b);
b.key2.push("大辉");
b.key2//小辉,小辉,大辉
a.key2//小辉,小辉
内存空间管理
JavaScript的内存生命周期
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放、归还
var a = 20; // 在内存中给数值变量分配空间
alert(a + 100); // 使用内存
a = null; // 使用完毕之后,释放内存空间
JavaScript的自动垃圾收集机制就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。
在JavaScript中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此a = null其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。
扩展一下null数据类型
null值表示一个空对象指针(引用),undefined 值是派生自 null 值的,所以typeof null ==> object(特殊)
undefined 是表示已声明但没有被赋值(未经初始化)的变量.
利用null和undefined就可以区分对象空指针和的变量
要定义的变量是将来保存对象用的,就应该明确地给该变量初始化为null。这样做不仅可以体现null作为空对象指针的惯例,而且也有助于进一步区分null和undefined。只要直接检查null值就可以知道相应的变量是否已经保存了一个对象的引用。
//准确判断
var exp = null;
!exp && typeof exp != "undefined" && exp != 0 ==>true
exp === null ==>true
//有误区的判断
!exp ==>true
//如果 exp 为 undefined,或数字零,或 false,也会得到与 null 相同的结果。
//同时判断 null、undefined、数字零、false 时可使用本法。
exp == null ==>true
//exp为 undefined 时,也会得到与 null 相同的结果。
//要同时判断 null 和 undefined 时可使用本法。
typeof exp == "null"
//为了向下兼容,exp 为 null 时,typeof null 返回 object,所以不能这样判断。
在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量。
参考文章之一:前端基础进阶(一):内存空间详细图解
网友评论