对闭包的理解、应用以及内存释放问题

1,144 阅读8分钟

对闭包的理解

闭包是函数运行时产生的机制,函数执行会形成一个全新的私有上下文,可以保护里面的私有变量和外界互不干扰(保护机制),但是大家认为的闭包需要当前上下文>不能被出栈释放,这样私有变量及它的值也不会被释放掉(保存机制)。

从性能角度讲,项目中应该减少对闭包的使用(因为闭包会产生不释放的栈内存,过多使用容易导致内存溢出或者降低性能)

问:闭包好不好?

答:大量应用闭包肯定会导致内存的消耗,但是闭包保护和保存作用,在真实开发中还是需要的,所以要合理使用。

问:闭包的作用是什么?

  1. 保护(私有变量和外界没有必然联系)
  2. 保存(形成不销毁的栈内存,里面的私有变量等信息保存下来了)

闭包的应用:

  1. 实战用途
  2. 高阶编程:柯理化 / 惰性函数 / compose函数
  3. 源码分析:JQ / LODASH / REACT(REDUX / 高阶组件 / HOOKS)
  4. 自己封装的插件组件

1、JQuery应用 -> 为了防止全局变量污染,JQ中的方法和变量需要用闭包保护起来。(导入jq后,它里面有大量的方法,如果不把这些方法保护起来,用户编写的方法很容易和jq方法名相同产生冲突,即全局变量污染)

/*  *****  JQuery源码  ***** */
(function(global,factory){
    //area1
    ...  
    //typeof window !== 'undefined'?window:this   ->   验证当前所处环境的全局对象是window还是global等
    //factory -> function(window,noGlobal){}
    factory(global);  // -> 执行area2-function
})(typeof window !== 'undefined'?window:this,function(window,noGlobal){
    //area2
    ...
    var jQuery = function(){
        ...
    }
    
    //通过给全局对象增加属性,把私有jquery方法暴露到全局作用域下供外面使用(<=>等价于return  jQuery)
    //外面需要使用函数中的私有内容,可以基于window.xxx和return xxx两种方式实现。
    window.jQuery = window.$ = jQuery; 
});
    
/*  *****  使用  ***** */
jQuery();  //相当于window.jQuery()
或
$();

真实项目中,我们一般把自己写的内容放到一个闭包中,这样可以有效防止自己的代码和别人代码产生冲突(全局变量污染:真实项目中是尽可能减少对全局变量的使用的);如果需要把自己的东西给别人用,基于return和window.xxx等方式暴露给别人即可。

2、基于let/const/class等创建变量,会把所在大括号(除对象的大括号之外)当作一个全新的私有块级作用域;

  • 函数执行会产生私有的栈内存(作用域/执行上下文)
  • let等也会产生私有的栈内存(var不会)

闭包练习题:

var ary = [1,2,3,4];
function fn(ary){
    ary[0] = 0;
    ary = [0];
    ary[0] = 100;
    return ary;
}
var res = fn(ary);
console.log(ary);    //[0,2,3,4]
console.log(res);	 //[100]

解析:

/*
	第一步:全局作用域-变量提升
	ary
	fn——AF0(堆内存,存储fn函数中的代码字符串)
	res
	第二步:代码执行
*/
var ary = [1,2,3,4];   //2.1赋值-引用数据类型-开辟堆内存AF1
//2.2变量提升阶段做处理
function fn(ary){
    /*
    	第三步:fn函数执行  传参ary,即AF1  形成私有作用域fn(AF1)
    	3.1——形参赋值,变量提升 ——> ary赋值AF1
    	3.2——代码执行
    		arr[0]=0;  AF1堆中的第一个元素值修改
    		ary = [0];  开辟新堆BF0,一个元素为0,修改ary对应的地址为BF0
    		ary[0] = 100;  修改BF0中元素为100
    		return ary;   -> return BF0;
    */
    ary[0] = 0;
    ary = [0];
    ary[0] = 100;
    return ary;
}   
var res = fn(ary);   //2.3-fn函数传实参,执行函数并将返回值赋给res    3.3-res值为BF0
console.log(ary);    //第四步:输出[0,2,3,4]
console.log(res);	 //第五步:输出[100]
/*
	var test = 自执行函数执行的返回结果;
	1、自执行函数执行
		test = AF0(小函数);
*/
var test = (function(i){
    	/*
    		1.1 形参赋值,变量提升 ——  i=2
    		1.2 return AF0(小函数);
    	*/
    	return function(){
            //传递5无用,没有形参
            alert(i*=2);  //i=i*2,i找上级=> 2*2=4     => "4"
        }
})(2)
test(5);
var a = 1;
var obj = {
    name:'tom'
}   //obj——AF0
function fn(){
    //形参赋值&变量提升  var a2;
    var a2 = a;  //a2 = 1;
    obj2 = obj;   //window.obj2 = AF0;  不是私有
    a2 = a;   //a2=1;
    obj2.name = 'jack';  //AF0堆中的name修改为jack
}
fn();
console.log(a)    //1
console.log(obj)  //{name:'jack'}

先形参赋值后变量提升

var a = 1;
function fn(a){
    /*
        1、形参赋值    a=1
        2、变量提升  
            var a;(无效,因为已有形参a)
            function a... -> 堆BF0; (声明无效,但需要给a赋值为函数)
    */
    console.log(a);   // 输出: 函数-f a() {}
    var a = 2;
    function a(){}
    console.log(a)    //2
}
fn(a);  //把全局a的值1当作实参传递给fn
/*
	1.全局作用域
		1.1变量提升:a b A-AF0   
		1.2代码执行:a=0  b=0  A(1)执行
			2.2执行完之后,A改为BF0
*/
var a=0,b=0;
function A(a){
    /*
    	2.A(1)执行形成私有作用域AAA => AF0(1)
    		2.1形参赋值&变量提升   形参赋值a=1
    		2.2代码执行 A=BF0,开辟堆存储代码字符串   A是全局window.A
    		2.3 alert(a++); 弹出字符串"1"——先弹出a在累加  a=2
    	3.A(2)执行形成私有作用域BBB => BF0(2)
    		3.1形参赋值&变量提升 形参赋值b = 2;
    		3.2代码执行 
    			alert(a+b++); 弹出字符串"4"——先弹出4在累加 b=3
    			=>a+2  //a是上级作用域AAA中的 b是私有
    			=>b++
    */
    A = function(b){
        alert(a+b++);
    }
    alert(a++);
}
A(1);
A(2);

创建函数

  • 开辟一个堆内存(16进制内存地址)
  • 声明当前函数作用域(函数在哪创建,那么它执行时候所需要查找的上级作用域就是谁
  • 把函数体中的代码当做字符串存储进去
  • 把堆内存的地址赋值给函数名/变量名

函数执行

  • 形成一个全新的私有作用域、执行上下文、私有栈内存(执行一次形成一个,多个之间也不会产生影响)
  • 形参赋值 & 变量提升
  • 代码执行(把所属堆内存中的代码字符串拿出来一行行执行)
  • 遇到一个变量,首先看它是否为私有变量(形参和在私有作用域中声明的变量是私有变量),是私有的就操作自己的变量即可,不是私有的则向上级作用域中查找...一直找到全局作用域为止 =>作用域链查找机制
  • 私有变量和外界的变量没有必然关系,可以理解为被私有栈内存保护起来了,这种机制其实就是闭包的保护机制

闭包应用

//在结构上存储元素的索引
<button index="0"></button>
<button index="1"></button>
<button index="2"></button>
//事件委托
var arr = ['a','b','c'];
document.body.onclick = function(ev){
	let target = ev.target,
        targetTag = target.tagName;
    if(targetTag == 'BUTTON'){
        var index = target.getAttribute('index');
        document.body.innerHTML = arr[index];
    }
}

闭包作用域练习:

function fun(n, o) {
    console.log(o);
    return {
        fun: function (m) {
            return fun(m, n);
        }
    };
}
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);   
//输出结果:undefined   0    1    1   

关于内存释放

堆内存释放

创建一个引用类型值,就会产生一个堆内存。

如果当前创建的堆内存不被其他东西占用了,浏览器会在空闲时查找每一个内存的引用情况,不被占用的都会被回收释放掉。

let obj = {
	age:18
}
let oop = obj;

此时obj和oop都占用了对象的堆内存,想要释放堆内存,需要手动解除变量盒值的关联(null——空对象指针)

obj = null;
oop = null;

浏览器的垃圾回收机制:

  1. 引用计数(以IE为主):在某些情况下会导致计数混乱,这样会造成内存不能被释放(内存泄漏)

  2. 检测引用(占用)(以谷歌为主):浏览器在空闲时会依次检测所有的堆内存,把没有被任何事物占用的内存释放掉,以此优化内存。

    手动释放内存,其实就是解除占用,手动赋值为null即可。

栈内存释放

  • 打开浏览器形成的全局作用域是栈内存;
  • 手动执行函数形成的私有作用域是栈内存;
  • 基于ES6中的let/const形成的块作用域也是栈内存;

全局栈内存:关掉页面的时候才会销毁;

私有栈内存:

  1. 一般情况下,函数执行完成,形成的私有栈内存就会被销毁释放掉(排除出现无限递归、死循环模式);

  2. 但是一旦栈内存中的某个东西(一般都是堆地址)被私有作用域以外的事物给占用了,则当前栈内存不能被立即释放销毁(特点:私有作用域中的私有变量等信息也保留下来了);

市面上认为的闭包:函数执行形成不能被释放的私有栈内存才是闭包;

//情况1
function fn(){
    ...         
}
fn();    //函数执行形成栈内存,执行完成栈内存销毁
    
//情况2
function x(){
    return function(){
        ...
    }
}
let f = x();    //f占用了x执行形成的栈内存中的一个堆地址,则x执行形成的栈内存不能被释放