前言
上周一直在做B站点首页,真的做的心力交瘁。今天一直解决不掉弹幕墙的BUG。无奈。休息下脑子,来总结一下上一篇提到的闭包的内容。
什么是闭包?
什么是闭包呢?先来看看JS大红书上的解释:闭包是指有权访问另一个函数作用域中的变量函数创建闭包的常见方式就是在一个函数内部创建另一个函数。
网上很多地方的教程里也是用这句话来进行的概括。
但其实闭包涉及到的知识还是非常多的。
在一个函数里创建的另一个函数就是闭包了吗?这个说法就不妥了。我们来深入的总结一下。
首先,我们要明白的是,闭包的概念基于作用域链,以及垃圾回收的机制。
上一篇的文章里面,已经提到当声明一个函数时,我们会创建这个函数的执行环境。在执行环境中会初始化scopechain即作用域链,此时的作用域链里只包含全局变量对象。当调用函数的时候,作用域链会将此函数的变量对象添加进来。当函数内部创建了别的函数时,那么作用域链中会将内部函数的活动对象添加到作用域链中。最终作用域链中得结构就是,一层套一层直到全局的变量对象。
当函数执行完毕后,局部的活动对象会被销毁。最后就会变成这样一个局面:在全局环境里定义的函数的作用域链,会一层一层剥开。最后内存中仅剩下全局的活动对象了。
但是闭包呢?也就是,全局环境下定义的函数的内层函数,亦或者是内层的内层函数呢?
这些函数的调用会有不同的结果。我们在调用非闭包函数并执行完函数时,作用域链中,非闭包函数的活动对象会从添加到销毁。但是闭包函数不同,当我们外层函数执行完毕后,闭包函数的作用域链会依旧访问父级的活动对象。那么如果我们仅仅是执行完外部函数,闭包函数的作用域链中,外部函数的活动对象是不会销毁的。。(但其作用域链被销毁)因为闭包函数还要调用他。只有当闭包函数销毁后,这个活动对象才会被销毁。销毁闭包函数需要通过手动设置来清除内存。故,闭包函数携带的其父级函数的作用域里的活动对象会一直占用内存。
为什么会产生闭包呢?我自己想了很久。。我自己的理解是这样的。
首先先提一下垃圾回收的机制。
主流的垃圾回收的策略是标记清除。标记清除会将进入环境的变量标记为”进入环境“。当变量离开环境时,标记为”离开环境“。
垃圾收集器会给存储在内存中所有的变量加上标记!然后,会去掉环境变量中,以及被环境变量引用的标记。之后,再被加上标记的变量会被视为准备删除的变量。原因是环境中的变量已经无法访问到这些变量了。
这个机制看上很拗口。其实说白了。离开环境后不用了就会被丢掉。标记的方式无关紧要。丢掉的东西会在内存中被清除。但是呢?函数内部的函数若是引用了父级函数的变量的话,当父级的函数执行完毕,工作流离开此环境的时候父级的活动对象若是清除了的话,那么闭包函数就无法访问到他们了。换句话说,父级的活动对象中被闭包函数引用的变量是还要继续使用的,若闭包函数不手动置空的话,它所引用的父级的活动对象中相应的变量就会被保留下来,一直放在我们的内存中占用着内存。
好了。关于闭包我的理解就是如此了。而它究竟有什么应用呢?
闭包的应用
关于应用。最先来说一下我在我做过的项目中应用的几个地方吧~
弹幕墙应用中的一段代码(与JS大红书上的例子是一样的,算是一个普片的用法了~):
$('video')[0].addEventListener('play',function(){
for(var i = 0;i < delayTimeArr.length;i++){
//(下面创建闭包函数以读取for循环内的数值
/*延迟发射弹幕,延迟时间与弹幕内容读取自本地存储,也久是上述数组取的内容*/
(function(num){
setTimeout(function(){
$('<div class="floatBullet" style="top:'+randomTop+'px'+'">'+contentArr[num]+'</div>').appendTo($('.border'));
},delayTimeArr[i]);
})(i);//闭包函数结束
}//for循环结束
});
这段代码的意思是,把历史弹幕的发送时间(基于视频的时间)取出来,然后把历史弹幕按照这个时间延迟发送出来。但是,由于ES5的标准中,只有函数才拥有自己的作用域。for循环结束后i=10,我们内部的函数是只能读取到 i = 10这个值的。因为保留在父级函数的活动对象中的i是父级函数执行完毕后i的值,也就是10。那么为了取到for循环中的每个i值,我么就需要再建立一个闭包函数,在第一层闭包中我们传入一个参数num,在第二个闭包也就是第二层闭包中我们引用这个参数,而这个参数我们在自执行的时候把i传递进去,也就是说,这个内部的闭包函数保留了第一层闭包函数活动对象中的num,而这个num是for在循环时所传递进来的每一个i的值。即:
(function(num){
setTimeout(function(){
$('<div class="floatBullet" style="top:'+randomTop+'px'+'">'+contentArr[num]+'</div>').appendTo($('.border'));
},delayTimeArr[i]);
})(i);
关于for和闭包还有自执行目前已经被ES6标准所淘汰了。。。。
接下来看看其他的作用把:
实现封装与模块化:
var person = function(){
var name = "moren;
return {
getName : function(){
return name;
}
setName : function(newName){
name = newName;
}
}
}();
console.log(person.name);//直接访问,结果为undefined
console.log(person.getName()); //default
person.setName("jozo");
console.log(person.getName()); //jozo
值得一提的是,自执行和闭包是没有必然联系的,只是他总是与闭包一起使用而已。
私有化变量:
只要是函数中的变量,平级与高级的函数是无法访问其中变量的,只能为自己和自己内部的函数所用。而函数内部getName和setName是可访问到内部的私有变量的。(两个函数又是能够被外部所调用的。)所以我们可以自由的定义特权函数去访问私有变量,而不通过特权函数是无法去访问这些私有变量的。
自执行的闭包函数可以模仿块级作用域:
在自执行的闭包函数内所定义的变量是不能被外部读取到的,所以利用函数的自执行,以及JS中只有函数才拥有执行环境的特质,我们能够模仿块级作用域。块级作用域的好处就是,它能够在执行完毕后,局部变量会被销毁,不会污染全局。在多人开发的大型应用程序中,这种方法能很好的避免不必要的麻烦。
静态私有变量:
在自执行函数内部,我们定义一个局部变量,再定义一个构造函数。接着通过原型来给构造函数添加方法。在添加的方法中,我们引用之前定义的局部变量。这样一来,所有的构造函数的实例,都能够通过添加的方法去访问到这个局部变量了!那么它就变成了一个静态的私有变量。若是通过this来添加方法的话,在实例化构造函数的时候,会重写属性,那么引用到的局部变量就属于每一个实例,而不是公有的了。(这里讲的有点晦涩。。是因为这是我再补了面向对象与继承的概念后才 写的。)
模块模式:
var Module = (function() {
// 下面函数是私有的,但可以被公开函数访问
function privateFunc() { … };
// 返回一个对象赋予Module
return {
publicFunc: function() {
privateFunc(); // publicFunc可以直接访问privateFunc
}
};
}());
这里COPY了一个例子,也是大红书上的例子,通过在自执行函数的内部返回一个字面量对象,也就是{}的形式表示的对象。自执行完毕后,我们就可以直接通过Module.publicFuc来使用封装好的方法了,并且还能够访问到模仿的块级作用域中的局部变量。
增强模块模式:
简单来说就是再定义一个变量:var obj = new object();然后在这个对象上去定义方法。最后将其返回。这里的 object可以是其他的实例。这种模式的运用场合显而易见。。就是希望返回的这个对象它已经是某一个构造函数的实例,同时,还需要添加其他的属性或者方法的时候。
关于ES6里的let
大红书里说过Javascript里没有块级作用域。但是ES6标准中就赋予了JS块级作用域!
我们只需要使用let来声明变量就可以了。
let声明的变量归属在他最近的一个大括号里。
这样一来,我们之前弹幕墙功能中的函数就可以进行一下改写了~
for(let i = 0;i < delayTimeArr.length;i++){
setTimeout(
function(){
$('<div class="floatBullet" style="top:'+randomTop+'px'+'">'+contentArr[num]+'</div>').appendTo($('.border'));
}
,delayTimeArr[i]);
}
我们不需要通过(function(){})()来模仿块级作用域了,因为let声明的变量i是属于靠的最近的大括号的。并且在执行完毕后,i会被回收~(就是这么简单)
关于let还有几个特性:
不存在变量提升现象。
不可以重复的声明。包括不能先var a=1;再let a=1;
不能let a= 1;let a =2;这样都会报错!还有不能再函数内部重复声明
例如
function func(a){
let a ;//报错
}
function func(a){
{
let a;//不报错
}
}
原因很简单。我们都知道函数传入的参数相当于在函数内部声明一个局部变量。再用Let当然会报错。然后如果加一个括号,那么Let就归属与它了~就不会报错了。
暂死性区域:
只要块级作用域存在Let命令,那么它所声明的变量就绑定这个区域,不再受外部影响。
var a = 1;
if (1 ){
a = 'abc';//ReferenceError
let tmp;
}
暂死区域是从块级作用域开始一直到Let命令结束。
这样一来。。typeof 也会报错了。只要你在死区里面。。。
最后有些死区很隐蔽:
function bar (x = y , y = 2){
return [x,y];
}
bar();//error
这是因为 y 是要赋值给x的,然而y在后面一句才声明,所以。。。
但是如果先恒明x =2 ; 再 y = x;这样就可以了
然后,块级作用域是可以嵌套的,只需要谨记,它尊崇let归属于最近的一个大括号就可以啦~
块级作用域与函数声明:
ES5中严格模式下,在块级作用域声明函数是不合法的。
但是ES6就相反了。
但是得注意的是,在块级作用域内声明的函数不会被提升!也无法被外部所引用!
结语
闭包看到一半发现自己大红书读的不透彻。又反过去过了一遍第六章= =,总算总结完了。
下一篇谈一谈面向对象吧~谈一些我自己对于JS中面向对象的理解(我觉得JS里一切都是函数。。。。肿么破。。。)