通过两个面试题解析JS底层原理
先来看一个例子:
setTimeOut(()=>{
console.log('set1');
});
new Promise((resolve,reject)=>{
console.log('p1')
resolve();
}).then(()=>{
console.log('then1');
})
console.log('1');
问:执行顺序?
答案:p1 --> 1 --> then1 --> set1 ;
new Promise() 的时候是同步执行。
微任务:
Promise,process.nextTick
宏任务:
整体的Scirpt代码,setTimeOut , setInterval
看看如下例子:
image.png
答案:pr1 ---> 2 ---> then1 ---> set1 --->then2 ---> then 4 ---> set2
解析过程:
确保微任务执行完了才去执行宏任务。
一 、js执行机制
image.png另一个例子:
问:下面的代码执行会出现什么样的结果?
var b = [];
for(var i=0;i<15;i++){
b.push(new Array(20*1024*1024));
}
结果,js内存不够了,V8引擎的内存溢出,崩溃了。
image.png
so,V8引擎内存有多大?
image.png
image.png
疑问:为什么64位的系统就只设置1.4G?设计的这么小?
原因:js回收的时候会中断执行。js回收100MB内存,则需要3ms。所以不能设计太大,因为会整个中断掉。
疑问:为什么1.4GB 是比较合适的?
原因:js的设计是为了跑前端脚本的,不是持续的,执行完了就没了。而不像后端,持续执行的,所以后端要大一些
,所以前端1.4GB是比较合适的。而且是足够了。
概念解释:
新生代:新变量存放的地方
老生代:没有被回收的老变量存放的地方
如果新生代要进行复制更改的话,就会被放到老生代里面去。算法:如果新生代的内存占用整个空间的25%的时候就会进行一次复制;
那么是如何进行复制的?
新生代有两个空间,当from空间大于25%的时候,会把from空间的活着的变量标记,并且复制到新生代To空间去。然后清空from空间。当To空间大于25%的时候,在此标记活着的变量,复制到from空间去。清空To。
啥时候新生代的变量会去老生代?
当新生代的空间大于25%,并且还活着的变量,复制过后,就去老生代空间。
二、内存如何回收?
image.png当一个变量 既不是全局,也不是局部且失去引用的时候,就会被回收。
如何查看内存
浏览器:window.performance
Node:process.memoryUsage();
image.png
Node 查看内存:
打开终端(前提是你安装了node):
输入 node 进入node环境
oricess.memoryUsage()
会显示如下:
{
rss:21258240,//总申请到的内存
heapTotal:5656576,//总堆内存
heapUsed:3051464,//已经使用的内存
external:1401021//Node 和 js 不同的这个。因为node的源码是C++写的,可以支配一些内存给C++,所以会有一些额外的内存来给C++使用。
}
接下来我们做个小测试:
function getme(){
var mem = process.memoryUsage();
var format = function(bytes){
return (bytes/1024/1024).toFixed(2)+"MB";
}
console.log("heapTotal:" + format(mem.heapTotal) + 'heapUsed:'+ format(mem.heapUsed));
}
var a =[];
var size = 20*1024*1024;
function b(){
var arr1 = new Array(size);
var arr2 = new Array(size);
var arr3 = new Array(size);
var arr4 = new Array(size);
var arr5 = new Array(size);
}
b();
getme();
setInterval(()=>{
a.push(new Array(20*1024*1024));
getme();
},1000);
每一秒钟执行一次;我们看下执行结果:
image.png
image.png
直到最后就崩了。。
梳理下:
最开始的时候有800多M,因为我们定义了5个局部变量并且没被回收。当到达1400的时候,发现要回收,回收了上面5个数组,当我们到最后,再去回收,发现全局只有一个a变量,无法被回收,所以最后崩溃了。
//5个全局变量 如下:
var size = 20*1024*1024;
var arr1 = new Array(size);
var arr2 = new Array(size);
var arr3 = new Array(size);
var arr4 = new Array(size);
var arr5 = new Array(size);
三、容易引发内存使用不当的操作
image.png1、反面教材1(滥用全局变量):
var size = 20*1024*1024;
var arr1 = new Array(size);
var arr2 = new Array(size);
var arr3 = new Array(size);
var arr4 = new Array(size);
var arr5 = new Array(size);
var arr6 = new Array(size);
var arr7 = new Array(size);
var arr8 = new Array(size);
var arr9 = new Array(size);
var arr10 = new Array(size);
var arr11 = new Array(size);
var arr12 = new Array(size);
var arr13 = new Array(size);
var arr14 = new Array(size);
var arr15 = new Array(size);
然后运行,直接报错,内存溢出。
如何正确的使用全局变量?
var size = 20*1024*1024;
var arr1 = new Array(size);
//使用完后释放
arr1 = undifined; //释放内存
额外小知识:
undifined 和 null 的区别:
null 是个关键字
undifined 是一个变量,和我们平时定义的 var a,bc 一样
image.png
我们测试如下代码:
var size = 20*1024*1024;
var arr1 = new Array(size);
arr1 = undifined;
var arr2 = new Array(size);
arr2 = undifined;
var arr3 = new Array(size);
arr3 = undifined;
var arr4 = new Array(size);
arr4 = undifined;
var arr5 = new Array(size);
arr5 = undifined;
var arr6 = new Array(size);
arr6 = undifined;
var arr7 = new Array(size);
arr7 = undifined;
var arr8 = new Array(size);
var arr9 = new Array(size);
var arr10 = new Array(size);
var arr11 = new Array(size);
var arr12 = new Array(size);
var arr13 = new Array(size);
var arr14 = new Array(size);
var arr15 = new Array(size);
运行后正常没有报错:
image.png
所以,尽量不要定义全局变量,如果一定要定义,那么定义完了,请释放。
2、反面教材2(缓存不限制):
缓存一定是全局的,如果不限制,早晚都会崩溃的。尤其是服务端,缓存一定要设置大小。
比较好的处理方式,给缓存加锁。
var b = [];
for(var i=0;i<15;i++){
b.push(new Array(20*1024*1024));
}
运行后是崩溃的
image.png
改成如下:
var b = [];
for(var i=0;i<15;i++){
if(b.length > 4){
a.shift();
}
b.push(new Array(20*1024*1024));
}
运行后正常,避免无限制的缓存造成内存崩溃。
缓存是你优化的好帮手,如果你一定要用的话,请在缓存上加一个锁。
3、反面教材3(操作大文件):
前端上传大文件,如果不处理 会直接卡死,
解决办法:切片上传;
使用node读取大文件的时候,如果是你使用
fs.readFile();
将会一次性将大文件读取到内存中,文件较大会发生卡顿或者崩溃。
4、扩展知识点,老生代的回收算法:
新生代算法,标记活的复制到另一个空间去,牺牲空间,换取时间。
老生代则不可能,因为有1.4G,老生代则是,标记-删除-整理的操作。
新生代:标记活着的变量
老生代:标记死亡变量,当内存大了,删除死亡的变量。整理,
[ ,1002,, 100]//如果不整理,数组这样的就不会是连续的了,如果不经常整理,数组就没办法继续添加了。数组必须是连续的。通过整理变成[ 1002,100,,],连续的数组。
5、扩展额外的例子:
var a ={n:1};
var b=a;
a.x = a = {n:2};
console.log(a);
console.log(b);
问:a,b分别是什么值?
答案:
image.png
解析:
对象是引用类型的--> 所有的对象复制其实是给了这个对象的引用地址;
假设 {n:2} 的地址是 1002,{n:1} 的地址 1000
当var b = a 的时候,a,b的地址都是1000;
当a.x=a={n:2}的时候,先会执行a.x = a ,这时候x的地址是1000,即为a.x = {n:1,x:1000的地址};
a又等于{a:2},所以a的地址变成了1002;
所以打出来的 a = {a:2}, b={n:1,x:{n:2}};
关注我,学习更多前端原理