基础-必会知识(中)
2 基础-必会知识(中)
2.1 请问你了解js中的闭包吗?
【考点映射】
-
js闭包
【频率】★★★★★
【难度】☆☆
【参考答案】
概念一:闭包是指有权访问另一个函数作用域中的变量的函数(概念出自《JavaScript高级程序设计》)
概念二:一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包,也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。(概念出自MDN)
可以简单理解为:闭包就是一个函数,一个外部函数通过调用函数并return返回出内部函数,此内部函数就是一个闭包
function f1(){
var n=999;
function f2(){ //f2函数就是闭包
alert(n);
}
return f2;// 重点在这里,将闭包函数作为返回值,做到f1能访问到f2的内部局部变量
}
var result=f1();
result(); // 999
此时f2函数形成了一个闭包,因f2函数里需要访问f1作用域下的n变量,但他们不处于同一个作用域,故两者相互牵引,需要输出n,f1中的变量n就必须存在,作用域链在f1中找到n,输出n时,垃圾回收机制会认为f2还没有执行完成,但此时作用域链查找已经到了f1作用域下,所以n的内存空间不会被垃圾回收机制清除
闭包优点:
-
可以读取函数内部的变量
-
延长局部变量寿命,不被垃圾回收机制销毁
-
封装变量(模仿块级作用域)
高频考题:
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i); //输出5个5
});
}
预期应该是输出0、1、2、3、4,但实际是输出5个5,因为setTimeout事件是被异步触发的,当事件被触发的时候,for循环早已经结束
可利用闭包解决该问题:将每次循环的i值封闭起来, 当沿着作用域链从内到外查找变量i时,会先找到被封闭在闭包环境中的i
//1、在setTimeout外部创建一个自执行函数,并将i当作参数传递进闭包
for(var i=0;i<5;i++){
(function(num){
setTimeout(function(){
console.log(num); // 输出0,1,2,3,4
}, num*1000);
}
)(i)
}
//2、在setTimeout内部函数创建一个闭包,并将i当作参数传递进去
for(var i=0;i<5;i++){
setTimeout(function(num){
return function(){ //用匿名函数打造一个num变量副本
console.log(num); // 输出0,1,2,3,4
}
}(i), i*1000);
}
闭包缺点:
-
闭包会导致变量不会被垃圾回收机制所清除,会大量消耗内存
-
使用不恰当可能会造成内存泄漏的问题
避免闭包引起的内存泄漏:
1、在退出函数之前,将不使用的局部变量全部删除或者赋值为null
将变量设置为null:切断变量与它此前引用的值之间的连接,当垃圾回收器下次运行时,会删除这些值并回收它们占用的内存
2、避免变量的循环赋值和引用
2.2 请问js垃圾回收机制是什么工作原理?
【考点映射】
-
js垃圾回收机制
【频率】★★★★★
【难度】☆
【参考答案】
js语言有 自动垃圾回收机制,执行环境会管理 代码执行过程中使用的内存,垃圾收集器会定期(周期性)找出不再继续使用的变量,然后释放其内存
不再使用的变量:生命周期结束的变量(局部变量),全局变量的生命周期直至浏览器卸载页面才会结束
栈内存 垃圾回收:
栈内存中的垃圾回收其实就是销毁执行栈中的执行上下文,栈顶就是正在执行函数的执行上下文, 当函数执行完毕后,执行栈中对应的执行上下文会被销毁
ESP 是执行栈中用来记录当前执行状态的指针, 当执行完一行后,ESP 指针下移,即该行对应的上下文被回收。 可理解为js引擎就是通过ESP指针的下移操作完成栈内存中的垃圾回收
堆内存 垃圾回收:
js中堆内存的垃圾回收主要建立在 代际假说 和 分代收集 两个概念上
代际假说:
-
大部分对象的存活时间都很短,分配完内存以后很快就变得不可访问
-
“不死”的对象,存活时间都很长
分代收集:
-
堆内存分为 新生代 和 老生代 两个区域
-
新生代区域:存放的都是存活时间比较短,占内存比较小的对象
-
老生代区域:存放的都是存活时间比较长,占内存比较大的对象
主垃圾回收器和副垃圾回收器:
新生代区域:副垃圾回收器
老生代区域:主垃圾回收器
这两个垃圾回收器的大致工作流程是相同的,可以简化为三步:
(1)、标记待回收的内存
(2)、垃圾内存回收
(3)、内存碎片整理(频繁的垃圾回收后,会产生很多不连续的内存空间,不利于后续数据的存储)
副垃圾回收器 工作流程
主要是对新生代区域进行垃圾回收,新生代区域的内存空间比较小,大约是 1~8M
采用的是 Scavenge 算法 进行垃圾回收,主要是将新生代区域 分成两部分:空闲区域 和对象区域, Scavenge 算法具体工作流程:
(1)、所有进入新生代区域新产生的对象都会存放到对象区域中
(2)、当对象区域被写满的时候会进行垃圾回收
(3)、垃圾回收器会标记垃圾数据(使用“标记清除算法”)
(4)、标记完成后对象区域会将有效数据按照一定顺序存放到空闲区域的一端
(5)、存放好后,对象区域和空闲区域会角色互换
(6)、清空当前的空闲区域的内存空间
其中,因为是对象区域的有效数据按照一定顺序放到了空闲区域中,所以也顺便完成内存碎片的整理
注意:新生代区域的空间很小,经常很快被填满,js有一个对象晋升策略解决这种情况:
对象晋升策略规定:两次垃圾回收还存活的对象就会被移动到老生代区域
主垃圾回收器 工作流程
对老生代区域进行垃圾回收,老生代区域的内存空间要大很多,用 Scavenge算法 效率明要低很多,还是按照以下三步进行垃圾回收:
(1)、通过标记清除算法,标记垃圾数据
(2)、标记垃圾数据后,主垃圾回收器开始进行垃圾回收,把可回收对象加入到空闲列表中
(3)、 剩下就是内存碎片整理,主垃圾回收器会将存活的对象移动到一端,然后清理掉边界以外的内存
【延伸考点】
1、什么是标记清除算法与引用计数算法?
两算法都是针对垃圾数据标记的
标记清除:js中最常用的垃圾回收方式,当变量进入环境时,(一般是在函数中声明一个变量),将这个变量标记为“进入环境”。而当变量离开环境时,则将其标记为“离开环境”。逻辑上讲,永远不能回收 进入环境的变量 所占用的内存,因为当执行流进入相应的环境,就可能会用到它们
function test(){
var a =10;//被标记 ,进入环境
var b =20;//被标记 ,进入环境
}
test();//执行完毕 之后 a、b又被标离开环境,被回收
引用计数:跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,这个值的引用次数是1;若同一个值又被赋给另一个变量,则该值的引用次数再加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1
当这个值的引用次数变成0时,则表示没有办法再访问这个值了,其占用的内存空间可回收
function test(){
var a ={};//a的引用次数为0
var b = a ;//a的引用次数加1,为1
var c = a;//a的引用次数再加1,为2
var b ={};//a的引用次数减1,为1
}
注意:引用计数算法是js早期的垃圾标记算法,现在几乎不怎么用,该算法存在一个问题:无法应对互相引用的情况,当两个对象互相引用时,就会永远无法被回收,从而造成内存泄漏。 基于这个问题,后来提出了标记-清除算法
2.3 请问js有哪几种常见的内存泄露情况?
【考点映射】
-
js内存泄露
【频率】★★★★
【难度】☆
【参考答案】
1、闭包
闭包可以延长局部变量寿命,若使用不当则会导致内存泄露
2、意外的全局变量
js中如果不用var声明变量,该变量将被视为window对象(全局对象)的属性,也就是全局变量,目前开发场景中:主要还是使用let和const较多
function foo(arg) {
bar = "this is a hidden global variable";
}
function foo(arg) {
window.bar = "this is an explicit global variable";
}
上面代码中两个函数是等价的,调用完函数后,变量仍然存在,会导致泄漏
如果不注意this的话,也可能发生内存泄露:
function foo() {
this.variable = "potential accidental global";
}
foo();// 没有对象调用foo, 也没有给它绑定this, 所以this是window
解决办法:加上“use strict”,启用严格模式来避免,严格模式会组织创建意外的全局变量
3、被遗忘的定时器或者回调
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
如上代码,若id为Node的元素从DOM中被移除,但定时器仍会存在,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放
4、没有清理的DOM元素引用
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
}
function removeButton(){
document.body.removeChild(document.getElementById('button'));
}
虽用removeChild移除了button,但是还在elements对象里保存着button的引用,DOM元素还在内存里面
2.4 请问你了解js的原型链吗?
【考点映射】
-
js原型链
【频率】★★★★★
【难度】☆☆
【参考答案】
与其他面向对象语言不同,ES6之前js没有引入类(class)的概念,js并非通过类而是直接通过构造函数来创建实例
构造函数与实例原型
在js中,每当定义一个函数(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象,并且这个属性是一
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
前端岗位面试求职攻略及真题解析~~
查看7道真题和解析