浅析js闭包

105 阅读7分钟

闭包

知识点:作用域 > 执行栈和执行上下文> 闭包> 闭包的作用> 闭包的应用> 闭包引起的问题

一、作用域和作用域链

在js中的变量不是在所有地方都能使用,一个变量生效的范围,就是这个变量的作用域。
作用域又分为全局作用域和局部作用域,

function fn1() {
let a = 1;
}
fucntion fn2 () {
let b = 2;
}
在声明的这两个函数中,fn1 和fn2 创建了两个私有作用域,即a和b只能对应在fn1 和fn2中使用。

每个函数都有自己的作用域,在需要查找变量或者函数的时候,从局部作用域到全局作用域查找,这样的作用域的集合叫做作用域链,

var a = 1 
function fn1 (){
	function fn2(){
		function fn3(){
			var a = 3;
			console.log(a);
		}
		// 执行fn2
		fn2();
	}
	//执行fn1
	fn1();	
}
//执行函数
fn();
在这个函数中 从fn3到fn1依次执行,就是一个局部作用域到全局作用域的查找。直到查找到a 这个过程就是利用了作用域链

二、执行上下文和执行栈

执行上下文就是js代码在解析和执行时的环境的抽象概念,js代码都是在执行上下文中解析和执行。
执行上下文有三种:
全局执行上下文:
就是默认的基础的执行上下文,不在函数的代码都在全局执行上下文中解析和执行。
有两个作用:
1创建了一个全局window对象。一个程序中只有一个全局执行上下文
2.将this指针指向全局对象。
函数执行上下文:
每次调用函数的时候,都会创建一个函数执行期上下文,每个函数都有自己的执行上下文,只有在调用的时候才会创建,当函数执行完成后,执行上下文立即销毁。
eval执行上下文在eval中的函数代码也会创建执行上下文,这个用的比较少。

执行栈
执行栈就是调用栈,是lifo(后进先出)的数据结构,用来存储代码运行时创建的所有执行上下文

当js引擎第一次遇到脚本时,会创建一个全局的执行上下文然后压入当前执行栈,每当引擎遇到一个函数调用,会为该函数创建一个新的执行上下文入栈的顶部。
函数执行结束后执行上下文从栈中退出,继续执行栈中的下一个上下文,

小结
1、函数内的变量之所以不能再函数外边引用,是因为在内存中形成的是一个私有占内存,不能再外边进行引用,
2、栈内存中存储值得过程为(vo阶段)。而执行代码的阶段为(ao阶段)–js编译过程
3.函数创建时,不执行的原因。
4由于引用类型的值太大。栈内存存放不下,只能存放在堆内存中

接下来就是重头戏闭包了

三、闭包

函数执行,形成一个私有的执行上下文,保护里边的私有变量不受外界的干扰,除了保护私有变量外,还可以保存一些内容,这种模式叫做闭包

function fn(){
	var name = 'zhangsan';
	function displayName(){
		alert (name);
}
	var myfn displayName;
}
var myfn = makefn()
myfn();

四、闭包的作用

保护>匿名执行函数>缓存>封装函数>类的模板机制
保护
1、团队开发时,每个开发者把自己的代码放在一个私有的作用域中,防止相互之间的变量名冲突,把需要提供给别人得方法,通过return或window.xxx的方式暴露在全局下。
2、jquery的源码中也是利用了这种保护机制。
3、封装私有变量

匿名执行函数

(function(a){
	console.log(a)
})(3)
//3

匿名执行函数只执行一次,执行完成后立即销毁,外部无法引用内部的变量,在执行完成后会被释放,这种机制不会污染全局对象

缓存

var CachedSearchBox = (function(){    
    var cache = {},    
       count = [];    
    return {    
       attachSearchBox : function(dsid){    
           if(dsid in cache){//如果结果在缓存中    
              return cache[dsid];//直接返回缓存中的对象    
           }    
           var fsb = new uikit.webctrl.SearchBox(dsid);//新建    
           cache[dsid] = fsb;//更新缓存    
           if(count.length > 100){//保正缓存的大小<=100    
              delete cache[count.shift()];    
           }    
           return fsb;          
       },    
     
       clearSearchBox : function(dsid){    
           if(dsid in cache){    
              cache[dsid].clearSelection();      
           }    
       }    
    };    
})();    
     
CachedSearchBox.attachSearchBox("input1"); 

重复调用一个对象的时候,可以利用闭包特性从缓存中调取该对象而不是单独去创建一个新的对象。

实现封装

var person = function(){    
    //变量作用域为函数内部,外部无法访问    
    var name = "default";       
       
    return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
}();    
     
console.log(person.name);//直接访问,结果为undefined    
console.log(person.getName());  // default   
person.setName("Tom");    
console.log(person.getName());    // Tom
  

在person之外的地方无法访问内部变量,二是通过闭包提供的接口来访问

实现面向对象中的对象,传统的对象语言都提供类的模板机制

function Person(){    
    var name = "default";       
       
    return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
};    
     
     
var john = Person();    
console.log(john.getName());    // default
john.setName("john");    
console.log(john.getName());    // john
     
var jack = Person();    
console.log(jack.getName());    // default 
jack.setName("jack");    
console.log(jack.getName());    // jack

john和jack都可以称为是Person这个类的实例,因为这两个实例对name这个成员的访问是独立的,互不影响的。

这点我理解的不是很好所以采用转载的方式

原作者链接

闭包有是三个特性:
1、函数嵌套函数。
2、函数内部可以引用函数外部的参数和变量
3、参数和变量不会被垃圾回收机制回收

五、闭包的应用

var num = 10;
var obj = {num;20};
obj.fn = (function (num) {
	this.num = num * 3;
	num ++ ;
	return function (n) {
		this.num += n ;
		num ++;
		console.log (num);
	}
}) (obj.num)
var fn = obj.fn;
fn(5)
obj.fn(10)
console.log(num,obj.num)
//22 23 65 30 

1.首先声明一个全局作用域(window)
2.第一步 :全局作用域下的变量提升(注:函数表达式,箭头函数,立即执行函数不会进行变量提升)
3.代码自上而下执行
4.正常的(数字、字符串)变量或常量直接赋值

如果遇到对象或者函数赋值就会在堆内存开辟一个内存空间,将字符串存进去。
。存储属性名
。将地址赋值给变量
。代码继续自上而下执行
如果遇到自执行函数,就将自执行函数的返回结果赋值给变量。
。函数执行,声明一个私有的作用域(变量都是私有的)。
。函形参赋值。
。变量提升。
。自执行函数代码自上而下继续执行。
。如果自执行函数没有主体,则 this 指向的 window;注意:看改变的值改变的是私有变量还
是其他变量。
。如果自执行函数返回一个函数(return)。
。开辟一个堆内存空间(将字符串存进去);
。将该堆内存的地址返回给 return
。所以自执行函数执行返回的变量地址指向 return 的地址(也就是执行开辟的堆内存空
间)。
。此时函数内部的函数被外部的变量引用着,所以这个私有的作用域不会销毁。
遇到函数执行。
。函数执行,形成一个私有的作用域(变量是私有的)。
。形参赋值。
。变量提升。
。函数自上而下执行。
。this 的指向遵循三个原则,以及注意私有作用域变量的作用范围
。函数执行完毕,进行作用域销毁

六、闭包引起的问题及解决方法

闭包常见的问题就是可能引起内存泄漏
由于js闭包是将函数变量保存在内存中,所以可能会造成内存溢出
setTimeout
setTimeout实质上是把代码推迟到指定时间后执行,
等当前脚本同步的任务或队列事件处理完成后,才会在指定时间内执行,不是立即执行

解决办法:

for (var i = 0; i < 5 ,i ++) {
	setTimeout(function () {
		console.log(i);
});
}
方法一:
1.在setTimeout外部创建一个立即执行函数的表达式,把i当做参数传递给闭包
for (var i = 0,i< 5;i ++){
	(
		function(num){
			setTimeout(function () {
				console.log(num);
			},num*1000);
	}
	)(i)
}
方法二:
2.在setTimeout内部函数创建一个闭包,并将i当做参数传递进去
for ( var i = 0; i<5; I++){
	setTimeout (function (num){
		return function () {
		consoe.log(num);
		}
	}(i),I*1000);
}