最近在理清一些JS中的基础概念,又开始重读JS高程,结合自己的工作经历,清除之前的技术盲区和一知半解,欢迎阅读并进行高质量的技术交流。
一 闭包和匿名函数
匿名函数是指没有名称的函数,如下面所示:
function (){
console.log("我是匿名函数");
}
function后面的函数名不存在,我们把这种函数叫匿名函数。
而闭包是只有权利访问另一个函数作用域中的变量的函数。如果你还没有了解作用域的概念,那么建议移步实例讲解JS中的作用域和作用域链,了解了JS中的作用域和作用域链后会帮助你理解闭包的概念。
创建闭包的常见方式就是在一个函数内部创建另一个函数。
function createCompareFunction(propername){
return function(obj1, obj2){
var value1 = obj1[propername];
var value2 = obj2[propername];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
}
var value1 = obj1[propername];
var value2 = obj2[propername];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
}
在上面的例子中,内部的函数中两行红色的内容引用了函数外部的变量propername。即使这个函数被返回了,在其他地方被调用,propername也可以被访问。之所以能访问这个变量,是因为内部函数的作用域链中包含外部函数createCompareFunction的作用域。
二 闭包的作用域链
上面代码中createCompareFunction的作用域链包括他自己的活动对象(0)和全局变量对象(1),下图中绿色线条所示。
匿名函数的作用域链有闭包的活动对象(0),creatCompareFunction(1)和全局变量对象(2),下图中红色线条所示。
通过上述的例子,发现闭包会携带包含它的函数的作用域,因此会占用比其他函数多的内存。同时闭包中可以将包含函数内部的私有变量返回。
三 闭包与变量
先来看一段代码:
在上面的代码中,返回的result是一个匿名函数组成的数组,执行每个函数,打印出的值都为10,在for循环中声明的 i 变量会存在变量提升,变量i的作用域在createFunctions中,当执行返回的的返回的函数时,这时候for循环已经执行完毕,i的值已经变成10,所以打印的是10。
所以闭包只能取得包含函数中任何变量的最后一个值,闭包保存的是整个变量对象,而不是某个特殊的变量。
上面的代码我们可以通过创建另外一个匿名函数强制让闭包行为符合预期,即依次打印的值为0,1,2...,代码如下:
在这个实例中,我们在循环里面将一个立即执行的匿名函数赋值给数组每一项。给这个立即执行的匿名函数传递变量i当前的值,因为参数传递是按值传递的,所以num是i的一个副本,不会随着循环中i的变化而变化。所以再执行返回的函数时,打印的是num当时的值。上面我们也说过,匿名函数执行时返回的值是包含函数中变量的最后一个值,num值就等于i某次循环的值,不会再改变。
四 闭包中的this
闭包中的this对象具有全局性,通常指向window。先来看下面的代码
上面代码中,定义了一个全局变量name,然后定义了一个对象Object,Object对象里有name属性和getNameFunc方法,返回一个匿名函数。
在JS中,每个函数被调用时都会自动提取两个特殊变量,this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动变量。在这个例子中,调用返回的匿名函数时,this的指向才确定,谁调用this指向谁,在这段代码里,window在调用。
console.log(Object.getNameFunc()());
//上面的写法等价于下面的
var newFun = Object.getNameFunc();
console.log(newFun());
如果想获得Object的name,那么在定义匿名函数之前把Object中的this指向存起来,在闭包中就可以访问这个变量下的属性和方法了。
五 内存泄漏
闭包会保存包含函数的整个变量,所以在闭包里可以访问包含函数的变量和方法。但是如果不及时清楚闭包中对包含函数的引用,则会引起内存泄漏。看下面的代码:
function assignHandler(){
var element = document.getElementById("el");
element.onclick = function(){
alert(element.id)
}
}
上面代码中onlick的回调函数中引用了外部变量element,所以只要匿名函数不销毁element的引用数至少为1,这样占用的内存永远不会被收回。
可以通过下面的方法对代码进行改写,从而避免内存泄漏。
function assignHandler(){
var element = document.getElementById("el");
var id = element.id;
element.onclick = function(){
alert(id)
}
element = null;
}
var id = element.id;
element.onclick = function(){
alert(id)
}
element = null;
}
六 闭包模仿块级作用域
在作用域的那篇文章里,我们说在JS中是没有块级作用域的,当时提到的一个方法是如果都是新浏览器,那么在ES6中可用通过声明let和const的变量来创建块级作用域。
匿名函数也可以用作块级作用域,语法如下:
(function(){
//这里是块级作用域
})();
以上代码定义并立即调用了这个匿名函数,圆括号包裹的匿名函数其实是一个函数表达式。如果不带圆括号,则解析器会以为function的语法报错,因为function后应该跟函数名。如果你还不太明白函数声明和函数表达式的区别,参考这篇文章JS中的函数声明和函数表达式
将函数声明转换成函数表达式,只要将函数用一对圆括号包裹即可。而匿名函数可以作为块级作用域。
上面在匿名函数外部的打印报错,说明count只在匿名函数块级作用域中有效。这种做法可以减少闭包浪费内存,因为没有指向匿名函数的引用,只要函数执行完毕,就可以销毁并释放内存了。
七 结语
闭包是JS中很重要的概念,理解之后对一些模块化的写法很有用,下一篇会整理模块化相关的知识,敬请期待。