闭包是函数以及在函数声明下的词法环境的结合

作者: 前端  发布:2019-10-06

闭包,是真的美

2018/04/11 · JavaScript · 闭包

原文出处: 张建成   

欢迎评论和star

写这篇文章时的心情是十分忐忑的,因为对于我们今天的主角:闭包,很多小伙伴都写过关于它的文章,相信大家也读过不少,那些文章到底有没有把JS中这个近似神话的东西讲清楚,说实心里,真的有,但为数不多。

写这篇文章的初衷:让所有看到这篇文章的小伙伴都彻彻底底的理解闭包 => 提高JS水平 => 能够写出更高质量的JS代码。

开文之所以说心情是忐忑的,就是怕达不到我写该文的初衷,但是我有信心同时我也会努力的完成我的目标。如行文中有丝毫误人子弟的陈述,欢迎大家指正,在这感激不尽。

我们开始吧:

相信众多JS的lovers都听说过这句话:闭包很重要但是很难理解

我起初也是这么觉得,但是当我努力学习了JS的一些深层的原理以后我倒觉得闭包并不是那么不好理解,反倒是让我感觉出一种很美的感觉。当我彻底理解闭包的那一刹那,心中油然产生一种十分愉悦感觉,就像**”酒酣尚醉,花未全开”**那种美景一样。

这就是闭包!

老规矩,上代码:

function foo() {

    var a = 2;

    function bar() {

    console.log( a );

}

   return bar;

}

   var baz = foo();

 baz(); // 2 —— 朋友,这就是闭包的效果。

这段代码非常清晰地展示了闭包,函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作一个值类型进行传递。在 foo() 执行后,其返回值赋值给变量 baz 并调用 baz(),也就是调用了内部函数bar()。bar()在自己定义的词法作用域以外的地方执行。由于bar()保持着队foo()词法作用域的引用,所以foo()在执行完后内部作用域也不会被程序销毁。

循环和闭包

for循环是常见的说明闭包的例子,也是我这样的小白非常容易错的地方。

for (var i = 0; i < 5; i++) {

setTimeout(function () {

console.log(i)

}, 0)

}

我们可能会简单的以为控制台会打印出‘ 0 1 2 3 4 ’,可事实却打印出了5个‘ 5 ’,这又是为什么呢?我们发现,setTimeout 函数时异步的,等到函数执行时,for循环已经结束了,此时的 i 的值为 5,所以 function() { console.log(i) } 去找变量 i,只能拿到 5。

所以,改进一下:

for (var i = 0; i < 5; i++) {

!function (i) {

setTimeout(function () {

console.log(i)

}, 0)}(i)

我们套用了一个立即执行函数,当i=0, 此时 function(i){} 此匿名函数中的 i 的值为 0,等到 setTimeout 执行时顺着外层去找 i,这时就能拿到 0。如此循环,就能拿到想要的 0 1 2 3 4。

作为小白,可能还是太明白什么是立即执行函数,下篇我们会详细介绍。这里我们先换个例子。

function constfunc(v){return function(){  return v;  };} // 这个函数返回一个总是返回v的函数

var funcs = []; // 创建一个空的数组

for(var i=0; i<10;i++){funcs[i] = constfunc(i)}

for(i=0;i<funcs.length;i++) {console.log(funcs[i]())}

这样就可以打印出0~9了。

不知道你明白了没有,反正我是明白啦~

今天给自己加个鸡蛋!

  自执行函数

拨开闭包神秘的面纱

我们先看一个闭包的例子:

function foo() { let a = 2; function bar() { console.log( a ); } return bar; } let baz = foo(); baz();

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
    let a = 2;
 
    function bar() {
        console.log( a );
    }
 
    return bar;
}
 
let baz = foo();
 
baz();

大家肯定都写过类似的代码,相信很多小伙伴也知道这段代码应用了闭包,but, Why does the closure be generated and Where is closure?

来,我们慢慢分析:

首先必须先知道闭包是什么,才能分析出闭包为什么产生和闭包到底在哪?

当一个函数能够记住并访问到其所在的词法作用域及作用域链,特别强调是在其定义的作用域外进行的访问,此时该函数和其上层执行上下文共同构成闭包。

需要明确的几点:

  1. 闭包一定是函数对象(wintercn大大的闭包考证)
  2. 闭包和词法作用域,作用域链,垃圾回收机制息息相关
  3. 当函数一定是在其定义的作用域外进行的访问时,才产生闭包
  4. 闭包是由该函数和其上层执行上下文共同构成(这点稍后我会说明)

闭包是什么,我们说清楚了,下面我们看下闭包是如何产生的。

接下来,我默认你已经读过我之前的两篇文章 原来JavaScript内部是这样运行的 和 彻底搞懂JavaScript作用域 , 建议先进行阅读理解JS执行机制和作用域等相关知识,再理解闭包,否则可能会理解的不透彻。

现在我假设JS引擎执行到这行代码

let baz = foo();

此时,JS的作用域气泡是这样的:

9159.com 1

这个时候foo函数已经执行完,JS的垃圾回收机制应该会自动将其标记为”离开环境”,等待回收机制下次执行,将其内存进行释放(标记清除)。

但是,我们仔细看图中粉色的箭头,我们将bar的引用指向baz,正是这种引用赋值,阻止了垃圾回收机制将foo进行回收,从而导致bar的整条作用域链都被保存下来

接下来,baz()执行,bar进入执行栈,闭包(foo)形成,此时bar中依旧可以访问到其父作用域气泡中的变量a。

这样说可能不是很清晰,接下来我们借助chrome的调试工具看下闭包产生的过程。

当JS引擎执行到这行代码let baz = foo();时:

9159.com 2

图中所示,let baz = foo();已经执行完,即将执行baz();,此时Call Stack中只有全局上下文。

接下来baz();执行:

9159.com 3

我们可以看到,此时bar进入Call Stack中,并且Closure(foo)形成。

针对上面我提到的几点进行下说明:

  1. 上述第二点(闭包和词法作用域,作用域链,垃圾回收机制息息相关)大家应该都清楚了
  2. 上述第三点,当函数baz执行时,闭包才生成
  3. 上述第四点,闭包是foo,并不是bar,很多书(《you dont know JavaScript》《JavaScript高级程序设计》)中,都强调保存下来的引用,即上例中的bar是闭包,而chrome认为被保存下来的封闭空间foo是闭包,针对这点我赞同chrome的判断(仅为自己的理解,如有不同意见,欢迎来讨论)

1. 什么是闭包?

作为一名前端无知小白,我猜MDN的中文版一定是机器翻译的,因为有时翻到上面的中文怎么看都不像是人话,比如闭包的定义:

Closures (闭包)是使用被作用域封闭的变量,函数,闭包等执行的一个函数的作用域。通常我们用和其相应的函数来指代这些作用域。(可以访问独立数据的函数)

能看得懂这个定义才真有鬼了。

好的,我们还是用蹩脚的英文来看看吧:

“A closure is the combination of a function and the lexical environment within which that function was declared.“

闭包是函数以及在函数声明下的词法环境的结合。

函数?变量?词法环境?

似乎感觉懂了一点点?

翻了下JS权威指南,里面说”Javascript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为闭包“。

所以我们从变量的作用域开始谈起。

经典案例

下面是一个经典的事件绑定例子:

<div id = "test">
    <p>栏目1</p>
    <p>栏目2</p>
    <p>栏目3</p>
    <p>栏目4</p>
</div>
 </body>
<script type="text/javascript">    
function bindClick(){
    var allP = document.getElementById("test").getElementsByTagName("p"),
    i=0,
    len = allP.length;        
    for( ;i<len;i++){
    allP[i].onclick = function(){
        alert("you click the "+i+" P tag!");//you click the 4 P tag!    
    }
    }
}
bindClick();//运行函数,绑定点击事件
</script>

上面的代码给P标签添加点击事件,但是不管我们点击哪一个p标签,我们获取到的结果都是“you click the 4 P tag!”。

我们可以把上述的JS代码给分解一下,让我们看起来更容易理解,如下所示。前面使用一个匿名函数作为click事件的回调函数,这里使用的一个非匿名函数,作为回调,完全相同的效果。

function bindClick(){
    var allP = document.getElementById("test").getElementsByTagName("p"),
    i=0,
    len = allP.length;
    for( ;i<len;i++){
    allP[i].onclick = AlertP;
    }
    function AlertP(){
    alert("you click the "+i+" P tag!");
    }
}
bindClick();//运行函数,绑定点击事件

这里应该没有什么问题吧,前面使用一个匿名函数作为click事件的回调函数,这里使用的一个非匿名函数,作为回调,完全相同的效果。也可以做下测试哦。

理解上面的说法了,那么就可以很简单的理解,为什么我们之前的代码,会得到一个相同的结果了。首先看一下for循环中,这里我们只是对每一个匹配的元素添加了一个click的回调函数,并且回调函数都是AlertP函数。这里当为每一个元素添加成功click之后,i的值,就变成了匹配元素的个数,也就是i=len,而当我们触发这个事件时,也就是当我们点击相应的元素时,我们期待的是,提示出我们点击的元素是排列在第几行。当click事件触发时,执行回调函数AlertP,但是当执行到这里的时候,发现alert方法中,有一个变量是未知的,并且在AlertP的局部作用域中,也没有查找到相应的变量,那么按照作用域链的查找方式,就会向父级作用域去查找,这里的父级作用域中,确实是有变量i的,而i的值,却是经过for循环之后的值,i=len。所以也就出现了我们最初看到的效果。

解决办法如下所示:

function bindClick(){
    var allP = document.getElementById("test").getElementsByTagName("p"),
  i=0,
  len = allP.length;
    for( ;i<len;i++){
    AlertP(allP[i],i);
    }
    function AlertP(obj,i){
    obj.onclick = function(){
        alert("you click the "+i+" P tag!");
    }
    }
}
bindClick();

这里,objiAlertP函数内部,就是局部变量了。click事件的回调函数,虽然依旧没有变量i的值,但是其父作用域AlertP的内部,却是有的,所以能正常的显示了,这里AlertP我放在了bindClick的内部,只是因为这样可以减少必要的全局函数,放到全局也不影响的。

这里是添加了一个函数进行绑定,如果我不想添加函数呢,当然也可以实现了,这里就要说到自执行函数了。可以跳到本文的自执行函数,也可以看参考引文的深度讲解:浅析作用域链–JS基础核心之一

闭包的艺术性

我相信这个世界上最美的事物往往就存在我们身边,通常它并不是那么神秘,那么不可见,只是我们缺少了一双发现美的眼睛。

生活中,我们抽出一段时间放慢脚步,细细品味我们所过的每一分每一秒,会收获到生活给我们的另一层乐趣。

闭包也一样,它不是很神秘,反而是在我们的程序中随处可见,当我们静下心来,品味闭包的味道,发现它散发出一种艺术的美,朴实、精巧又不失优雅。

9159.com 4

细想,在我们作用域气泡模型中,作用域链让我们的内部bar气泡能够”看到”外面的世界,而闭包则让我们的外部作用域能够”关注到”内部的情况成为可能。可见,只要我们愿意,内心世界和外面世界是可以相通的

3. 变量的生命周期

我们知道的事情有:

1. 一个局部变量当定义该变量的函数调用结束时,该变量就会被垃圾回收机制回收而销毁。再次调用该函数时又会重新定义了一个新变量。

  1. 按照代码书写时的样子,内部函数可以访问函数外面的变量。

那么,如果在函数内部声明一个内部函数,并将内部函数作为值返回,调用外部函数之后,内部函数保持对外部函数词法作用域的引用,这样会发生什么呢?由于内部函数作为值返回了出去,所以外层函数执行完毕,其词法作用域中的变量也不会被销毁。

 

闭包的应用的注意事项

闭包,在JS中绝对是一个高贵的存在,它让很多不可能实现的代码成为可能,但是物虽好,也要合理使用,不然不但不能达到我们想要的效果,有的时候可能还会适得其反。

  • 内存泄漏(Memory Leak)JavaScript分配给Web浏览器的可用内存数量通常比分配给桌面应用程序的少,这样做主要是防止JavaScript的网页耗尽全部系统内存而导致系统崩溃。因此,要想使页面具有更好的性能,就必须确保页面占用最少的内存资源,也就是说,我们应该保证执行代码只保存有用的数据,一旦数据不再有用,我们就应该让垃圾回收机制对其进行回收,释放内存。

    我们现在都知道了闭包阻止了垃圾回收机制对变量进行回收,因此变量会永远存在内存中,即使当变量不再被使用时,这样会造成内存泄漏,会严重影响页面的性能。因此当变量对象不再适用时,我们要将其释放。

    我们拿上面代码举例:

function foo() { let a = 2; function bar() { console.log( a ); }
return bar; } let baz = foo(); baz();
//baz指向的对象会永远存在堆内存中 baz = null;
//如果baz不再使用,将其指向的对象释放

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f6bea87da5441991997-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da5441991997-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da5441991997-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da5441991997-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da5441991997-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da5441991997-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da5441991997-7">
7
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da5441991997-8">
8
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da5441991997-9">
9
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da5441991997-10">
10
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da5441991997-11">
11
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da5441991997-12">
12
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da5441991997-13">
13
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da5441991997-14">
14
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da5441991997-15">
15
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f6bea87da5441991997-1" class="crayon-line">
 function foo() {
</div>
<div id="crayon-5b8f6bea87da5441991997-2" class="crayon-line crayon-striped-line">
     let a = 2;
</div>
<div id="crayon-5b8f6bea87da5441991997-3" class="crayon-line">

</div>
<div id="crayon-5b8f6bea87da5441991997-4" class="crayon-line crayon-striped-line">
     function bar() {
</div>
<div id="crayon-5b8f6bea87da5441991997-5" class="crayon-line">
         console.log( a );
</div>
<div id="crayon-5b8f6bea87da5441991997-6" class="crayon-line crayon-striped-line">
     }
</div>
<div id="crayon-5b8f6bea87da5441991997-7" class="crayon-line">

</div>
<div id="crayon-5b8f6bea87da5441991997-8" class="crayon-line crayon-striped-line">
     return bar;
</div>
<div id="crayon-5b8f6bea87da5441991997-9" class="crayon-line">
 }
</div>
<div id="crayon-5b8f6bea87da5441991997-10" class="crayon-line crayon-striped-line">

</div>
<div id="crayon-5b8f6bea87da5441991997-11" class="crayon-line">
 let baz = foo();
</div>
<div id="crayon-5b8f6bea87da5441991997-12" class="crayon-line crayon-striped-line">

</div>
<div id="crayon-5b8f6bea87da5441991997-13" class="crayon-line">
 baz(); //baz指向的对象会永远存在堆内存中
</div>
<div id="crayon-5b8f6bea87da5441991997-14" class="crayon-line crayon-striped-line">

</div>
<div id="crayon-5b8f6bea87da5441991997-15" class="crayon-line">
 baz = null; //如果baz不再使用,将其指向的对象释放
</div>
</div></td>
</tr>
</tbody>
</table>

关于内存泄漏,推荐
[阮一峰老师博客](http://www.ruanyifeng.com/blog/2017/04/memory-leak.html)。

2. 变量作用域

先来说几个概念:

全局变量:在所有作用域都可访问的变量,在函数外定义的变量就是全局变量

局部变量:在函数中使用关键字声明的变量,它的作用域只在声明该变量的函数内,在函数外面是访问不到该变量的。

词法作用域:词法作用域也叫静态作用域,也就是说函数的作用域在函数定义的时候就决定了,而不是调用的时候决定。JavaScript采用静态作用域,变量的作用域完全由写代码期间函数声明的位置来定义的。

话不多说,上代码:

代码1:

var func = function(){

     var a = 'closure'

       console.log(a);         // closure

}

func();

console.log(a); // Uncaught ReferenceError: a is not defined

局部变量a只能在函数内部使用,函数调用结束时,该变量就会被垃圾回收机制回收而销毁

代码2:

var value = 1;

function foo() {

    console.log(value);

}

​function bar() {

    var value = 2;

    foo();

}

bar(); 

foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

自执行函数

也就是在函数名后添加括号,函数就会自执行。在绑定事件时,像我这样的初学者有时会犯如下的错误,window.onclick = ab();这样函数ab一开始就会执行。正确的做法应该将ab后的括号去掉。而这种加括号的做法其实是把ab函数运行的结果赋值给点击事件。

下面两个例子清楚地反映了函数赋值后的情况。

1:

function ab () {
    var i=0;
    alert("ab");
    return i;
}
var c=ab();//执行ab函数
alert(typeof c+"      "+c);//number  0

2:

function ab () {
    var i=0;
    alert("ab");
    return i;
}
var c=ab;//只赋值
alert(typeof c+"      "+c);//function  function ab () {var i=0;alert("ab");return i;}

注:但是这个函数必须是函数表达式(诸如上文提到的赋值式函数),不能是函数声明。详细请看:js立即执行函数:(function(){...})()与(function(){...}())

文中主要讲到匿名函数的自执行方法,即在function前面加!、+、 -甚至是逗号等到都可以起到函数定义后立即执行的效果,而()、!、+、-、=等运算符,都将函数声明转换成函数表达式,消除了javascript引擎识别函数表达式和函数声明的歧义,告诉javascript引擎这是一个函数表达式,不是函数声明,可以在后面加括号,并立即执行函数的代码(jq使用的就是这种方法)。举例如下所示。

(function(a){
    console.log(a);   //firebug输出123,使用()运算符
})(123);

(function(a){
    console.log(a);   //firebug输出1234,使用()运算符
}(1234));

!function(a){
    console.log(a);   //firebug输出12345,使用!运算符
}(12345);

+function(a){
    console.log(a);   //firebug输出123456,使用+运算符
}(123456);

-function(a){
    console.log(a);   //firebug输出1234567,使用-运算符
}(1234567);

var fn=function(a){
    console.log(a);   //firebug输出12345678,使用=运算符
}(12345678)

其作用就是:实现块作用域。

javascript中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据javascript函数作用域链的特性,使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以( function(){…} )()内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”。代码如下:

function test(){ 
(function (){ 
for(var i=0;i<4;i++){ 
} 
})(); 
alert(i); //浏览器错误:i is not defined
} 
test();

 可以对比最开始介绍作用域时候的代码。

 

闭包的应用

  1. 模块一个模块应该具有私有属性、私有方法和公有属性、公有方法。而闭包能很好的将模块的公有属性、方法暴露出来。
var myModule = (function (window, undefined) { let name = "echo";
function getName() { return name; } return { name, getName }
})(window); console.log( myModule.name ); // echo console.log(
myModule.getName() ); // echo

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f6bea87da9603634463-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da9603634463-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da9603634463-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da9603634463-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da9603634463-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da9603634463-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da9603634463-7">
7
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da9603634463-8">
8
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da9603634463-9">
9
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da9603634463-10">
10
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da9603634463-11">
11
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da9603634463-12">
12
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da9603634463-13">
13
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87da9603634463-14">
14
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87da9603634463-15">
15
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f6bea87da9603634463-1" class="crayon-line">
var myModule = (function (window, undefined) {
</div>
<div id="crayon-5b8f6bea87da9603634463-2" class="crayon-line crayon-striped-line">
 let name = &quot;echo&quot;;
</div>
<div id="crayon-5b8f6bea87da9603634463-3" class="crayon-line">

</div>
<div id="crayon-5b8f6bea87da9603634463-4" class="crayon-line crayon-striped-line">
 function getName() {
</div>
<div id="crayon-5b8f6bea87da9603634463-5" class="crayon-line">
 return name;
</div>
<div id="crayon-5b8f6bea87da9603634463-6" class="crayon-line crayon-striped-line">
 }
</div>
<div id="crayon-5b8f6bea87da9603634463-7" class="crayon-line">

</div>
<div id="crayon-5b8f6bea87da9603634463-8" class="crayon-line crayon-striped-line">
 return {
</div>
<div id="crayon-5b8f6bea87da9603634463-9" class="crayon-line">
 name,
</div>
<div id="crayon-5b8f6bea87da9603634463-10" class="crayon-line crayon-striped-line">
 getName
</div>
<div id="crayon-5b8f6bea87da9603634463-11" class="crayon-line">
 }
</div>
<div id="crayon-5b8f6bea87da9603634463-12" class="crayon-line crayon-striped-line">
})(window);
</div>
<div id="crayon-5b8f6bea87da9603634463-13" class="crayon-line">
 
</div>
<div id="crayon-5b8f6bea87da9603634463-14" class="crayon-line crayon-striped-line">
console.log( myModule.name ); // echo
</div>
<div id="crayon-5b8f6bea87da9603634463-15" class="crayon-line">
console.log( myModule.getName() ); // echo
</div>
</div></td>
</tr>
</tbody>
</table>

“return”关键字将对象引用导出赋值给myModule,从而应用到闭包。
  1. 延时器(setTimeout)、计数器(setInterval)这里简单写一个常见的关于闭包的面试题。
for( var i = 0; i &lt; 5; i++ ) { setTimeout(() =&gt; { console.log(
i ); }, 1000 * i) }

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f6bea87dad912221241-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87dad912221241-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87dad912221241-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87dad912221241-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87dad912221241-5">
5
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f6bea87dad912221241-1" class="crayon-line">
for( var i = 0; i &lt; 5; i++ ) {
</div>
<div id="crayon-5b8f6bea87dad912221241-2" class="crayon-line crayon-striped-line">
 setTimeout(() =&gt; {
</div>
<div id="crayon-5b8f6bea87dad912221241-3" class="crayon-line">
 console.log( i );
</div>
<div id="crayon-5b8f6bea87dad912221241-4" class="crayon-line crayon-striped-line">
 }, 1000 * i)
</div>
<div id="crayon-5b8f6bea87dad912221241-5" class="crayon-line">
}
</div>
</div></td>
</tr>
</tbody>
</table>

答案大家都知道:**每秒钟输出一个5,一共输出5次**。

那么如何做到**每秒钟输出一个数,以此为0,1,2,3,4**呢?

我们这里只介绍闭包的解决方法,其他类似块作用域等等的解决方法,我们这里不讨论。



for( var i = 0; i &lt; 5; i++ ) { ((j) =&gt; { setTimeout(() =&gt; {
console.log( j ); }, 1000 * j) })(i) }

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f6bea87db1013292990-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87db1013292990-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87db1013292990-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87db1013292990-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87db1013292990-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87db1013292990-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87db1013292990-7">
7
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f6bea87db1013292990-1" class="crayon-line">
for( var i = 0; i &lt; 5; i++ ) {
</div>
<div id="crayon-5b8f6bea87db1013292990-2" class="crayon-line crayon-striped-line">
 ((j) =&gt; {
</div>
<div id="crayon-5b8f6bea87db1013292990-3" class="crayon-line">
 setTimeout(() =&gt; {
</div>
<div id="crayon-5b8f6bea87db1013292990-4" class="crayon-line crayon-striped-line">
 console.log( j );
</div>
<div id="crayon-5b8f6bea87db1013292990-5" class="crayon-line">
 }, 1000 * j)
</div>
<div id="crayon-5b8f6bea87db1013292990-6" class="crayon-line crayon-striped-line">
 })(i) 
</div>
<div id="crayon-5b8f6bea87db1013292990-7" class="crayon-line">
}
</div>
</div></td>
</tr>
</tbody>
</table>

“setTimeout”方法里应用了闭包,使其内部能够记住每次循环所在的词法作用域和作用域链。

由于setTimeout中的回调函数会在当前任务队列的尾部进行执行,因此上面第一个例子中每次循环中的setTimeout回调函数记住的i的值是for循环作用域中的值,此时都是5,而第二个例子记住的i的数为setTimeout的父级作用域自执行函数中的j的值,依次为0,1,2,3,4。
  1. 监听器
var oDiv = document.querySeletor("#div");
oDiv.addEventListener('click', function() { console.log( oDiv.id );
})

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f6bea87db4035872148-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87db4035872148-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87db4035872148-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f6bea87db4035872148-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f6bea87db4035872148-5">
5
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f6bea87db4035872148-1" class="crayon-line">
var oDiv = document.querySeletor(&quot;#div&quot;);
</div>
<div id="crayon-5b8f6bea87db4035872148-2" class="crayon-line crayon-striped-line">
 
</div>
<div id="crayon-5b8f6bea87db4035872148-3" class="crayon-line">
oDiv.addEventListener('click', function() {
</div>
<div id="crayon-5b8f6bea87db4035872148-4" class="crayon-line crayon-striped-line">
 console.log( oDiv.id );
</div>
<div id="crayon-5b8f6bea87db4035872148-5" class="crayon-line">
})
</div>
</div></td>
</tr>
</tbody>
</table>

=- 关于闭包,我觉得我说清楚了,你看清楚了吗?留言告诉我吧 -=

如果你觉得写的还不是很烂,请关注我的 github 吧,让我们一起成长。。。

1 赞 3 收藏 评论

9159.com 5

闭包


**  9159.com ,[块作用域与函数作用域](

全局作用域和局部作用域

通常来讲这块是全局变量与局部变量的区分。 参考引文:JavaScript 开发进阶:理解 JavaScript 作用域和作用域链

全局作用域:最外层函数和在最外层函数外面定义的变量拥有全局作用域。

  1)最外层函数和在最外层函数外面定义的变量拥有全局作用域

  2)所有末定义直接赋值的变量自动声明为拥有全局作用域,即没有用var声明的变量都是全局变量,而且是顶层对象的属性。

  3)所有window对象的属性拥有全局作用域

局部作用域:和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所以在一些地方也会看到有人把这种作用域称为函数作用域。

代码部分请参照引文。

  [代码块]()  **

 

 

 

 

  [全局作用域和局部作用域]()

**  作用域中的声明提前**

函数声明与赋值

[闭包]() 

**函数声明与赋值**

代码块

JavaScript中的代码块是指由<script>标签分割的代码段。JS是按照代码块来进行编译和执行的,代码块间相互独立,但变量和方法共享。如下:

<script type="text/javascript">//代码块一
var test1 = "我是代码块一test1";
alert(str);//因为没有定义str,所以浏览器会出错,下面的不能运行
alert("我是代码块一");//没有运行到这里
var test2 = "我是代码块一test2";//没有运行到这里但是预编译环节声明提前了,所以有变量但是没赋值
</script>
<script type="text/javascript">//代码块二
alert("我是代码块二"); //这里有运行到
alert(test1); //弹出"我是代码块一test1"
alert(test2); //弹出"undefined"
</script>

上面的代码中代码块一中运行报错,但不影响代码块二的执行,这就是代码块间的独立性,而代码块二中能调用到代码一中的变量,则是块间共享性。

但是当第一个代码块报错停止后,并不影响下一个代码块运行。当然在下面的例子中,虽然代码块二中的函数声明预编译了,但是在代码块1中的函数出现Fn函数为定义错误(浏览器报错,并不是声明未赋值的undefined),说明代码块1完全执行后才执行代码块2。

<script type="text/javascript">//代码块1
Fn(); //浏览器报错:"undefined",停止代码块1运行
alert("执行了代码块1");//未运行
</script>
<script type="text/javascript">//代码块2
alert("执行了代码块2");//执行弹框效果
function Fn(){ //函数1
alert("执行了函数1");
}
</script>

所以js函数解析顺序如下:
  step 1. 读入第一个代码块。
  step 2. 做语法分析,有错则报语法错误(比如括号不匹配等),并跳转到step5。
  step 3. 对var变量和function定义做“预编译处理”(永远不会报错的,因为只解析正确的声明)。
  step 4. 执行代码段,有错则报错(比如变量未定义)。
  step 5. 如果还有下一个代码段,则读入下一个代码段,重复step2。
  step6. 结束。

:需要在页面元素渲染前执行的js代码应该放在<body>前面的<script>代 码块中,而需要在页面元素加载完后的js放在</body>元素后面,body标签的onload事件是在最后执行的。

<script type="text/javascript">
alert("first");
function Fn(){
alert("third");
}
</script>
<body onload="Fn()">
</body>
<script type="text/javascript">
alert("second");
</script>

目录

块作用域与函数作用域

函数作用域是相对块作用域来进行解释的,其和局部作用域是一个意思。参考引文:JavaScript的作用域和块级作用域概念理解

块作用域:任何一对花括号{}中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是无效的,我们称之为块级作用域。

函数作用域:在函数中的参数和变量在函数外部是无法访问的。JavaScript 的作用域是词法性质的(lexically scoped)。这意味着,函数运行在定义它的作用域中,而不是在调用它的作用域中。下文会解释。

9159.com 69159.com 7

 1 //C语言 
 2 #include <stdio.h> 
 3 void main() 
 4 { 
 5 int i=2; 
 6 i--; 
 7 if(i) 
 8 { 
 9 int j=3; 
10 } 
11 printf("%d/n",j); 
12 }

View Code

运行这段代码,会出现“use an undefined variable:j”的错误。可以看到,C语言拥有块级作用域,因为j是在if的语句块中定义的,因此,它在块外是无法访问的。

9159.com 89159.com 9

1 function test(){ 
2         for(var i=0;i<3;i++){};    
3         alert(i); 
4         } 
5         test();

View Code

运行这段代码,弹出"3",可见,在块外,块中定义的变量i仍然是可以访问的。也就是说,JS并不支持块级作用域,它只支持函数作用域,而且在一个函数中的任何位置定义的变量在该函数中的任何地方都是可见的。

闭包(Closure)

闭包对于初学者来说很难,需要学习很多很多才能领会,所以也是先把作用域链和匿名函数的知识作为铺垫。我这里的闭包内容属于基础篇,以后可能会贴一些更为核心的内容。我这里参照了大神们的讲解来说。参考引文:学习Javascript闭包(Closure),JavaScript 匿名函数(anonymous function)与闭包(closure),浅析作用域链–JS基础核心之一

闭包是能够读取其他函数内部变量的函数,所以在本质上,闭包将函数内部和函数外部连接起来的一座桥梁。

闭包是在函数执行结束,作用域链将函数弹出之后,函数内部的一些变量或者方法,还可以通过其他的方法引用。

两个用处:一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

为了帮助理解,我找了几个例子:

1.(阮一峰老师的讲解)

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

2.(某大神)

function foo() { 
var a = 10; 
function bar() { 
a *= 2; 
return a; 
} 
return bar; 
} 
var baz = foo(); 
alert(baz()); //20
alert(baz()); //40    
alert(baz()); //80

var blat = foo(); 
alert(blat()); //20

现在可以从外部访问 a; 
a 是运行在定义它的 foo 中,而不是运行在调用 foo 的作用域中。 只要 bar 被定义在 foo 中,它就能访问 foo 中定义的变量 a,即使 foo 的执行已经结束。也就是说,按理,"var baz = foo()" 执行后,foo 已经执行结束,a 应该不存在了,但之后再调用 baz 发现,a 依然存在。这就是 JavaScript 特色之一——运行在定义,而不是运行的调用。 
其中, "var baz = foo()" 是一个 bar 函数的引用;"var blat= foo()" 是另一个 bar 函数引用。 
用闭包还可实现私有成员,但是我还没理解,所以就先不贴出来,想看的请参照参考引文:JavaScript 匿名函数(anonymous function)与闭包(closure)。

 

结束

第一次写这么长的文章,大部分是引用,但是所有内容都是亲自实践并思考后才贴出来,作为初学者可能有解释和引用不当的地方,还请大家指出。有问题的地方还请各位老师同学多来指教探讨。

再次感谢所有引文作者,知识的增长在于传播,感谢辛苦的传播者。

 

参考文献:

JavaScript 开发进阶:理解 JavaScript 作用域和作用域链,

JavaScript的作用域和块级作用域概念理解,

Js作用域与作用域链详解,

浅析作用域链–JS基础核心之一,

javascript 匿名函数的理解(透彻版),

JS中函数执行顺序的问题,

js立即执行函数:(function(){...})()与(function(){...}()), 

学习Javascript闭包(Closure),

JavaScript 匿名函数(anonymous function)与闭包(closure)

 

 

作用域中的声明提前

var scope="global";  //全局变量
function t(){  
    console.log(scope);  
    var scope="local" ;//局部变量
    console.log(scope);  
            }  
t();

 

(console.log()是控制台的调试工具,chrome叫检查,有的浏览器叫审查元素,alert()弹窗会破坏页面效果)

第一句输出的是: "undefined",而不是 "global"

第二讲输出的是:"local"

第二个不用说,就是局部变量输出"local"。第一个之所以也是"local",是因为Js中的声明提前,尽管在第4行才进行局部变量的声明与赋值,但其实是将第4行的声明提前了,放在了函数体顶部,然后在第4行进行局部变量的赋值。可以理解为下面这样。

var scope="global";//全局变量
function t(){
    var scope;//局部变量声明
    console.log(scope);
    scope="local";//局部变量赋值
    console.log(scope);
}
t();

 

具体细节可以查阅犀牛书(《JavaScript权威指南》)中的详细介绍。

**  声明式函数、赋值式函数与匿名函数**

作用域链(Scope Chain)

当代码在一个环境中执行时,会创建变量对象的的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是一个函数,则将其活动对象作为变量对象。参考引文:Js作用域与作用域链详解,浅析作用域链–JS基础核心之一

num="one";
var a = 1;  
function t(){  //t函数的局部作用域,可以访问到a,b变量,但是访问不到c变量
     var num="two"; 
     var b = 2;
    function A(){ //A函数局部作用域,可以访问到a,b,c变量 
        var num="three"; //局部变量与外部变量重名以局部变量为主
        var c = 3;
        console.log(num); //three 
            }  
    function B(){  //B函数局部作用域,可以访问到a,b变量,访问不到c变量
        console.log(num); //two 
            }  
    A();  
    B();  
}  
t();

当执行A时,将创建函数A的执行环境(调用对象),并将该对象置于链表开头,然后将函数t的调用对象链接在之后,最后是全局对象。然后从链表开头寻找变量num。

即:A()->t()->window,所以num是”three";

但执行B()时,作用域链是: B()->t()->window,所以num是”two";

另外,有一个特殊的例子我觉得应该发一下。利用“JavaScript 的作用域是词法性质的(lexically scoped)。这意味着,函数运行在定义它的作用域中,而不是在调用它的作用域中。” 这句话,解释了下面的例子。

var x = 10;

function a() {
console.log(x);
}

function b () {
var x = 5;
a();
}

b();//输出为10

虽然b函数调用了a,但是a定义在全局作用域下,同样也是运行在全局作用域下的,所以其内部的变量x,向上寻找到了全局变量x=10;所以b函数的输出为10;

更深层次的讲解请参照:JavaScript 开发进阶:理解 JavaScript 作用域和作用域链。

作用域

声明式函数、赋值式函数与匿名函数

匿名函数:function () {}; 使用function关键字声明一个函数,但未给函数命名,所以叫匿名函数,匿名函数有很多作用,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序或创建闭包等等。下文会讲到。

JS中的函数定义分为两种:声明式函数与赋值式函数。

<script type="text/javascript">
Fn(); //执行结果:"执行了声明式函数",在预编译期声明函数及被处理了,所以即使Fn()调用函数放在声明函数前也能执行。
function Fn(){ //声明式函数
alert("执行了声明式函数");
}
</script>

<script type="text/javascript">
Fn(); //执行结果:"Fn is not a function"
var Fn = function(){ //赋值式函数
alert("执行了赋值式函数");
}
</script>

JS的解析过程分为两个阶段:预编译期(预处理)与执行期。
预编译期JS会对本代码块中的所有声明的变量和函数进行处理(类似与C语言的编译),此时处理函数的只是声明式函数,而且变量也只是进行了声明(声明提前)但未进行初始化以及赋值。所以才会出现上面两种情况。

当正常情况,函数调用在声明之后,同名函数会覆盖前者。

<script type="text/javascript">
function Fn(){ //声明式函数
alert("执行了声明式函数");
}
var Fn = function(){ //赋值式函数
alert("执行了赋值式函数");
}
Fn();//执行结果:"执行了赋值式函数",同名函数后者会覆盖前者
</script>

 同理当提前调用声明函数时,也存在同名函数覆盖的情况。

<script type="text/javascript">
Fn(); //执行结果:"执行了函数2",同名函数后者会覆盖前者
function Fn(){ //函数1
alert("执行了函数1");
}
function Fn(){ //函数2
alert("执行了函数2");
}
</script> 

 

作用域(scope)

**作用域链**

本文由9159.com发布于前端,转载请注明出处:闭包是函数以及在函数声明下的词法环境的结合

关键词: