js进阶系列(五):作用域闭包(一)

/ 0评 / 0

说起这个闭包呐,真的是js里面一个很重要但是又很难掌握的一个知识点了。以前说起闭包就心塞,因为对这种只可意会不可言传的知识,每个人都有自己的理解,但是理解的不同又会导致对闭包不够深入。关于这个闭包,我会尝试让读者大爷们能弄清楚,这是一个挑战!。

在红皮书《JavaScript高级程序设计》是这样定义闭包的:闭包是指有权访问另一个函数作用域中变量的函数。对于闭包难道是一个特殊的函数吗?也可以这么理解,但是闭包不仅仅指的是一个函数。下面我们来深入探讨。

如果你了解了之前关于词法作用域的讨论,那么闭包的概念几乎是不言自明的。对于词法作用域,可以参考我之前写的文章。闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意 识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿 来识别、利用和影响闭包的思维环境。

当函数可以访问所在的词法作用域的时候,就会形成闭包。即使函数是在当前词法作用 域之外执行。

function foo(){
    var a=1;
    function bar(){
        console.log(a);
    }
    bar();
}

foo();    //1

基于词法作用域的查找规则,函数 bar() 可以访问外部作用域中的变量 a,但是有人在想,这会是闭包吗?技术上来讲,也许是。但根据前面的定义,确切地说并不是。最准确地用来解释 bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却 是非常重要的一部分!)

在上面的代码片段中,函数 bar() 具有一个涵盖 foo() 作用域的闭包 (事实上,涵盖了它能访问的所有作用域,比如全局作用域)。也可以认为 bar() 被封闭在 了 foo() 的作用域中。为什么呢?原因简单明了,因为 bar() 嵌套在 foo() 内部。但是这个方式,虽然对于词法作用域我们很清楚,但是我们对于闭包的观察室很难的,我们很难弄清比闭包里面发生了什么。

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

这就是一个最典型的闭包了, 函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作 一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),实 际上只是通过不同的标识符引用调用了内部的函数 bar()。

bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方 执行。在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃 圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很 自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此 没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一 直存活,以供 bar() 在之后任何时间进行引用。bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的 词法作用域。

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到 闭包。

function foo() {     var a = 2; 
    function sub() {   
      console.log( a ); // 2   
     } 
    bar(sub); 
} 
function bar(fn) { 
    fn(); // 这就是闭包!
 }

把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部 作用域的闭包就可以观察到了,因为它能够访问 a。

当然啦,我们知道函数的参数也可以是函数,通过间接传递函数可以观察到闭包;

var fn; 
function foo() {   
  var a = 2; 
    function baz() {         console.log( a );     } 
    fn = baz; // 将 baz 分配给全局变量 } 
function bar() {   
  fn(); //这就是闭包!
 } 
foo(); 
 
bar(); // 2

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包。

我相信,看到这里对闭包应该会清晰一点了。接下来我们继续。

相信对setTimeout()都很熟悉,举个例子:

function sayMsg(msg){
    setTimeout(function time(){    //为了更好的实践,我们使用具名函数,当然也可以用匿名函数
        console.log(msg);
    },1000);
}

sayMsg('Hello World!');

这个函数运行后,会在一秒后输出“Hello World!”,sayMsg运行一毫秒后,它的内部作用域并不会消失,timer 函数依然保有 sayMsy(..) 作用域的闭包。

深入到引擎的内部原理中,内置的工具函数 setTimeout(..) 持有对一个参数的引用,这个 参数也许叫作 fn 或者 func,或者其他类似的名字。引擎会调用这个函数,在例子中就是 内部的 timer 函数,而词法作用域在这个过程中保持完整。这就是闭包。

抛出结论  本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!闭包内容有点多,我需要整理下,下次继续探讨闭包!

学习笔记,欢迎交流。

发表评论

电子邮件地址不会被公开。 必填项已用*标注