项目中遇到了关于js内存,引用类型,gc机制相关的问题,记录下来。
首先复现一下代码:
const option = {
yAxis: [{
type: 'value',
......
}]
};
option.yAxis.push(option.yAxis[0]);
option.yAxis[0].min = 1;
option.yAxis[1].min = 2;
发现option.yAxis下面的min的值都是2;第一反应是以为之前的代码写的有问题,所以打断点,打日志,发现都正常,然后想到了有可能是内存指针导致的,然后可以做以下两种修改。
const option = {
yAxis: [{
type: 'value',
......
}]
};
option.yAxis.push(JSON.parse(JSON.stringify(option.yAxis[0])));
option.yAxis[0].min = 1;
option.yAxis[1].min = 2;
或者
const option = {
yAxis: [{
type: 'value',
......
}]
};
option.yAxis.push(option.yAxis[0]);
option.yAxis[0] = {min: 1};
option.yAxis[1] = {min: 2};
以上两种写法都可以规避内存指针引用导致的数据问题。
剖析以下问题的本质吧:
- JS基本数据类型:Null,Undefined,Number,Boolean,Symbol,String
- JS复杂数据类型:Object Array (也算是object)
当我们在代码里面:
var num = 123;
var obj = { name: 'obj' };
var obj_copy = obj;
此时会开辟两块内存空间,一个存储123,一个存储 { name: 'obj' }; 其中是num的指针会直接指向栈内存中的123,obj的指针会指向堆内存中的 { name: 'obj' },obj_copy的引用指针会存放在栈内存中;
如果我们编辑以下代码:
var num = 123;
var obj = { name: 'obj' };
var obj_copy = obj;
obj.name = 'xxx';
那么我们会发现obj_copy的name也会变成xxx,其实就是因为产生了指针以引用,指向的是同一块内存空间;
如果编辑以下代码:
var num = 123;
var obj = { name: 'obj' };
var obj_copy = obj;
obj = {name: 'xxx'};
那么我们会发现obj_copy的name还是obj,这是因为obj = {name: 'xxx'};会产生一块新的内存空间,然后obj会产生一次引用,obj_copy的引用跟该引用没有任何毛线关系。那么我们项目中的问题就迎刃而解了。
写到这里,突然想到了es6 的const 变量申明:
const object = { name: 'object'};
const num = 5;
object.name = 'new_object';
num = 4;
我们会发现object的name确实变成了字符串new_object,但是num=4会报错,为什么呢?其实这个问题跟堆栈内存没有关系,还是跟引用数据类型有关;
看下面的代码:
let numA = 1;
let numB = numA;
numA = 2;
console.log(numB );
我们发现numB的值仍然是1,其实基本数据类型也是存在引用的,只是基本数据类型无法像object一样去更改某个key的值而已,就比如一座房子和一把板凳,如果你改变了房子或者凳子,那么他就是实实在在地改变了,如果你只是改变了一座房子内部的一部分,它仍然是房子。
说到这里,不得不提一下es6的 WeekMap了(也是借用了阮大的例子吧,哈哈哈!)
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
WeekMap 是一种弱引用,也就是说,该节点的引用计数是1,如果element设置为null,Weakmap 保存的这个键值对,也会自动消失。
说到这里,就要说一下javascript的垃圾回收机制了,分为两种类型吧,一种是标记清除 ,一种是引用计数。
let fn = function (){
let a = 1;
return a;
}
fn();
在执行fn函数时,变量a被标记为进入环境,在函数没有被执行结束之前,是不能释放该变量所指向的内存的,当函数执行完之后,变量会被标记为离开环境,则会被gc回收
let fn = function (){
let a = 1;
return a;
}
let back = fn();
如果代码写成这样的话,变量a所占用的内存是不会被gc的,因为在外部存在了引用,虽然a已经离开了fn的执行环境,但是a的引用计数是2,所以不会被gc回收清除。
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
上述情况也是objectA 和B都离开了函数环境,但是因为存在循环引用,所以引用计数都不为0,所以内存就不会得到回收,在个别情况下,需要手动回收。
另外,gc时,会阻塞主线程,所以平常写代码的时候一定要注意相关问题,务必规避内存泄漏的相关问题。