什么是闭包
要知道闭包
是什么东西,首先我们要知道JavaScript的作用域
,其实在我的理解看来,JS的闭包无非是对作用域的一种应用。
JS作用域
关于JS变量的作用域无非就两种:全局变量
以及局部变量
。
全局作用域
对于定义在最外层的变量,我们认为它具有全局性,即在所有内部的函数里都能够对其进行访问。
<script>
var global = "Hello";
function innerFun() {
console.log(global);
}
innerFun();
</script>
Output:Hello
全局变量除了上述定义方法之外,还有另外一种方式需要注意,就是在函数内部定义变量不加 var
关键字,这样默认会定义成为一个全局变量。(不推荐这样)
<script>
function innerFun() {
innerVar = "inner";
}
innerFun();
console.log(innerVar);
</script>
Output:inner
局部作用域
和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的,最常见的例如函数内部。
<script>
function innerFun() {
var innerVar = "inner";
}
console.log(innerVar);
</script>
Output:innerVar is not defined
变量提升
要知道变量提升,我们需要先知道JS引擎执行代码的过程,分为两步,首先会解释代码,也就是先通篇扫描JS代码,把所有的声明都提升
到顶端,然后才会执行代码。
<script>
console.log(a);
var a = "test";
</script>
JS引擎会把这段代码翻译成如下:
<script>
var a;
console.log(a);
a = "test";
</script>
Output:undefined
作用域链
红皮书中对作用域链的描述是这样的:
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问
作用域链的最前端始终是当前执行的代码所在环境的变量对象
。如果这个环境是函数,则将其活动对象
作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象。作用域链的下一个变量对象来自外层的包含环境,而在下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
这里面有很多概念,比如什么是执行环境
、变量对象
、活动对象
等。先说明一下这些概念的理解。
执行环境(Execution Context)
执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象
,环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外层的一个执行环境。在Web浏览器中,全局执行环境被认为是window对象
,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境知道应用程序退出–例如关闭网页或浏览器—时才会被销毁)
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。
<script>
var scope = "global";
function fun1(){
return scope;
}
function fun2(){
return scope;
}
fun1();
fun2();
</script>
上述代码执行情况:
变量对象(Variable Object)
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。 比如在全局的环境中有如下代码:
<script>
var scope = "global";
function fun1(){
console.log("this is fun1");
}
</script>
那么这时我们的全局环境的变量对象为:
活动对象(activation object)
当函数被调用的时候,活动对象将会被创建。这个对象中包含形参
和arguments对象
。活动对象之后会作为函数执行环境的变量对象来使用。换句话说,活动对象除了变量和函数声明之外,它还存储了形参和arguments对象。
<script>
function add(value1, value2){
return value1 + value2;
}
</script>
比如我们调用add()
函数,传入参数value1=1,value2=2
,那么此时这个函数的活动对象为:
作用域链详解
上面我们提到过,当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数对象处于第三位……直至作为作用域终点的全局执行环境。
<script>
function add(value1, value2){
return value1 + value2;
}
var result = add(1,2);
</script>
上面的代码定义了add()函数,然后又在全局作用域中调用了它。当调用add()时,会创建一个包含arguments、value1、value2的活动对象。全局执行环境的变量对象(包含result和add)在add()执行环境的作用域链中则处于第二位。下图包含了上述关系的add()函数执行时的作用域链。
对于add()函数而言,其作用域链中包含两个变量对象:本身函数的活动对象和全局变量对象。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
标识符解析是沿着作用域链一级一级地搜索标识符地过程。搜索过程始终从作用域链地前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)—-《JavaScript高级程序设计》
下面这段代码
<script>
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn();
</script>
outer()
内部返回了一个inner
函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象),具体如下图:
通常情况,当一个环境中的所有代码都执行完了的时候,这个执行环境会被弹出环境栈,并且其中存储的变量和函数也会随之销毁。但是上面的那种内部函数情况有所不同,当outer执行完了,outer的执行环境被销毁,但是其关联的活动对象依然在内存中,并没有随之销毁,因为他的活动对象被内部的inner函数的作用域链所引用。具体如下:
像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)
闭包
闭包用通俗的话来说,就是函数中的函数,用于延长作用域链,使得我们可以通过作用域链可以使用函数本身外部的变量。
变量持久化
闭包一个非常重要的特点就是可以使外部变量持久化。看下面的例子:
<script>
function outer() {
var count = 1;
change = function(){
count += 1;
}
function inner(){
console.log(count);
}
return inner;
}
var fn = outer();
fn();
change();
fn();
</script>
Output:1
2
调用了两次fn函数,分别输出了1,2,说明变量count并没有被销毁,而是持久化的存在内存中的。
封装变量&收敛权限
在实际开发过程中,闭包应用比较多的一点就是用来封装变量,收敛权限。
<script>
function isFirstLoad(){
var list = [];
return function(option){
if(list.indexOf(option) > -1){
console.log("数据已经存在");
}else{
list.push(option);
console.log("数据插入成功");
}
}
}
var fn = isFirstLoad();
fn(1);
fn(1);
fn(2);
</script>
Output:数据插入成功
数据已经存在
数据插入成功
外界想要访问这个list变量,只能通过isFirstLoad()这唯一提供的接口。并且list的操作规则我们已经写好了,外界直接调用传入参数而已。
总结
由于使用闭包会导致父函数活动对象不被销毁,也就是说父函数的变量都会被持久化到内存中,所以对于内存消耗比较大,因此不能滥用闭包,否则会造成网页的一些性能问题。