对闭包的理解:
闭包是由函数以及创建该函数的词法环境组成,形式上看就是函数内的子函数。
因为js有自动的垃圾回收机制,当变量不被引用时就会通过特殊的算法回收(一般是标记清除和引用计数两种方法),当一个函数执行完后作用域会被销毁,而闭包可以访问父级作用域,父级函数中的变量在闭包中被引用,那么就不会被垃圾回收;所以闭包可以实现在函数外部访问内部的局部变量,并且可以使让这些变量的值始终保持在内存中。
从内存来看闭包:
当JS引擎判断形成闭包,就会在堆内存中开辟一块空间,里面有一个闭包对象closure,这个对象里包含闭包用到的外部变量,之后要用这些变量的时候,就会去到堆里面的closure对象里面找。
理解练习:
例一:
var x = 10;
function fn(){
console.log(x);
}
function show(f){
var x = 20; //因为闭包是由函数及创建该函数的词法环境组成,而fn创建的词法环境中x=10
(function(){
f();
})()
}
show(fn); //10
例二:
function a() { var i = 0; function b() { alert(++i); } return b; } var c = a(); c(); //1 i=0,第一次调用c 执行++i,输出1 c(); //2 因为是闭包,i变量在b()中引用不会在c()执行完后释放内存,所以再次c(),++i就是1+1=2
例三:
var add = function(x) { var sum = 1; var tmp = function(x) { sum = sum + x; return tmp; } tmp.toString = function() { return sum; } return tmp; } alert(add(1)(2)(3)) // 6
第一次调用传入了参数1,没有使用,此时sum的值是1;然后返回tmp函数
第二次调用tmp函数传入 2,sum的值是1+2=3;
第三次重复调用tmp函数传入3,sum的值是3+3=6;隐式自动调用tmp的toString()弹出6
例四:
var i = 0;
function outerFn(){
function innerFn(){
i++;
console.log(i);
}
return innerFn
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();//1
inner2();//1
inner1();//2
inner1();//3
inner2();//2
inner2();//3
因为闭包找到的是同一地址中父级函数中对应变量最终的值。
例五:
function fun(){
//1. 找受保护的变量: 外层函数的局部变量
var n=999;
//2. 外层函数共抛出哪些内层函数
nAdd=function(){n++};
return function(){ console.log(n) };
}
var get=fun();//fun的AO(n=999)
//var nAdd: function(){n++}
//get: function(){ console.log(n) };
get();//999
nAdd();//AO(n=1000)
get();//1000
因为nAdd没有被定义过,强行赋值会使JS将nAdd定义为全局变量,所以他也会被返回到外部,故也将执行
例六:
for(var i = 0;i < 5;i++){
setTimeout(function (){
console.log(i++);
},4000)
}
console.log(i);
for循环并不是一个函数,所以没有函数作用域,var声明的变量也不存在块级作用域,所以i就是个全局变量。
顺序执行,先执行for,setTimeout异步宏任务放任务队列等待执行
当for循环完i=5,执行console.log(i)输出5
然后执行任务队列的五个setTimeout宏任务,因为是闭包i的值保存不被释放,所以接下来输出5,6,7,8,9
正常实现从0增加输出:
1.立即执行函数,每一次都会创建函数执行环境得到对应的结果
for(var i = 0;i < 5;i++){
(function (x) {
setTimeout(function (){
console.log(x++);
},4000)
})(i);
}
2.setTimeout 的第三个参数
for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
3.使用 let 定义 i
for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
内存溢出和内存泄漏:
内存溢出:当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
内存泄露:占用的内存没有及时释放
常见的内存泄漏:
1.没有及时清理的计时器或回调函数
原因:定时器中有dom的引用,即使dom删除了,但是定时器还在,所以内存中还是有这个dom。
解决:手动删除定时器和dom。
2.意外的全局变量。
原因:全局变量,不会被回收。
解决:使用严格模式避免。
function fn () { b = new Array[1000000] a = [] //不小心没有var定义,这时候a变量是全局的 } fn()
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();
3.闭包,函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长,容易造成内存泄露。
原因:闭包可以维持函数内局部变量,使其得不到释放。
解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
4.dom清空或删除时,事件未清除导致的内存泄漏。
原因:虽然别的地方删除了,但是对象中还存在对dom的引用
解决:手动删除。
5.子元素存在引用引起的内存泄漏
原因:div中的ul li 得到这个div,会间接引用某个得到的li,那么此时因为div间接引用li,即使li被清空,也还是在内存中,并且只要li不被删除,他的父元素都不会被删除。
解决:手动删除清空。
怎样避免内存泄露:
1)减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;
2)注意程序逻辑,避免“死循环”之类的 ;
3)避免创建过多的对象 原则:不用了的东西要及时归还。
关于垃圾回收机制:
垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。
通常情况下有两种实现方式:标记清除,引用计数。
标记清除:
工作原理:是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程:
1. 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
2. 去掉环境中的变量以及被环境中的变量引用的变量的标记。
3. 再被加上标记的会被视为准备删除的变量。
4. 垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
引用计数:
工作原理:跟踪记录每个值被引用的次数。
工作流程:
1. 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是1。
2. 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1.
3. 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减1.
4. 当引用次数变成0时,说明没办法访问这个值了。
5. 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。