文章

关于JavaScript闭包的理解

JavaScript

关于 JavaScript 闭包的理解

关于JavaScript闭包的理解

理解闭包,需要函数知识的铺垫,而非读概念就能明白。

如何快乐并有趣地理解闭包呢?

​ 首先,我想感谢一下《Head First JavaScript 程序设计》的作者:Eric T. Freeman 和 Elisabeth Robson. 感谢这两位能写出如此棒的图书 ,至少在我看来,当我读完关于闭包的这一章时,我想我已经明白了闭包。如果你买了这本书并认真地读完,我想你会知道我说的是什么东西了。


​ 好了,先来说说匿名函数吧。匿名函数是没有名字的函数表达式,它就好比函数表达式的右手端。如:function () { ... } 函数表达式可以有形参,当然匿名函数也可以有形参。在代码中我们定义了一个函数,然后就可以在代码的任何地方通过函数名或者变量名,去调用它。在 JavaScript 中,函数可以是值这个值实际上是指向函数的引用,而非真的是个数值,当然我们可以这么去想,帮助我们去理解。函数与其他值不同的地方在于,我们可以调用它。函数属于一等值,一等值是啥?我们可以将一等值赋给变量或存储在数组和对象等数据结构中、将其传递给函数、从函数中返回它们。既然函数也属于一等值,那么函数就可以做上面说的事。在 JS 中,我们尽量不要认为函数是特殊的,将它认为值有助于我们理解。


​ 接着来讲讲作用域,讲作用域之前建议先去看看并熟悉浏览器执行 JavaScript 代码的工作原理,这有助于理解接下来要讲的东西。我们知道,浏览器分两遍去执行 JavaScript 代码,第一遍分析代码中是否存在函数声明,若有,则定义这些函数声明创建的函数;处理完所有的函数声明后,再按从上到下的顺序执行 JavaScript 代码,若在执行的过程中发现了有函数表达式,再创建其引用并完成接下来的工作。通过上述讲的工作原理,我们能发现函数声明创建的函数总是先定义,因为浏览器第一遍执行时就是在寻找函数声明。这意味着可讲函数声明放在代码的任何地方,且可以在任何地方去调用它,因为它不可能没有被定义。对于在代码的任何地方,函数声明创建的函数都是已定义的,这被称为提升(hoisting)。而函数表达式呢?得等到第二遍时才能被定义。假如我们在函数中再嵌套一个函数时,又会怎么影响作用域呢?我们来看个例子:

var migrating = ture;
var fly = function(num) {
    var sound = "Flying";
    functiong wingFlapper() {
        console.log(sound);
    }
    for(var i = 0; i < num; i++){
         wingFlapper();
    }
};
function quack(num) {
     var sound = "Quack";
     var quacker =  function(){
     console.log(sound);
    };
    for(var i = 0; i < num; i++) {
        quacker();
    }
}
if (migrating) {
    quack(4);
    fly(4);
}

在代码顶层定义的函数是全局的,而在函数中定义的函数是局部的。何为代码的顶层?我们知道函数有函数名和函数体,那么顶层就是指除函数体外的,也就是函数名这一行了。顶层是相对于每一个函数而言的,不是所有的代码,这要清楚。那么在上面这个例子中,代码的顶层定义的函数有 fly、quack,因此这两个函数的作用域都是全局的,当然 fly 得当浏览器执行函数表达式后,才是已定义的(函数表达式定义的函数,只有在浏览器执行到其所在的那一行后,才是被定义的)。而其他函数,wingFlapper的作用域仅在整个 fly 函数内,quacker的作用域仅在整个 quack 函数内,但仅在这个函数表达式(quacker是函数表达式)被执行后且在到达函数 quack 末尾前,它才是已定义的。

​ 总的来说:在函数内部,如果你使用函数声明创建了一个嵌套函数,那么在这个函数的函数体的任何地方,嵌套函数都是已定义的;如果你使用函数表达式创建了一个嵌套函数,则在这个函数的函数体内,仅当函数表达式执行完之后,嵌套函数才是已定义的。


​ 说到作用域,再来谈谈词法作用域词法意味着只需要查看代码的结构就可以确定变量的作用域。来看个例子:

var just = "Oh";            ————>这里定义了一个全局变量just
function f(){
    var just = "Hey";    ————>这里定义了一个同名的局部变量just
    return just;
}
var result = f();
console.log(result);

​ 首先我们要明白:当一个全局变量与局部变量同名时,局部变量将遮住全局变量。让我们将视线转到代码,函数 f 被调用后,将返回 just,那么这个 just 引用的是哪个变量呢?词法作用域总是在最近的函数作用域内查找 just,如果在这个作用域内没有找到,再在全局作用域内查找。也就是说,词法作用域规定了先在局部作用域内查找有没有同名的变量,如果没有,那么就再在全局作用域内查找。我觉得,这个词法作用域就是对”当一个全局变量与局部变量同名时,局部变量将遮住全局变量 “这句话的一个补充吧,词法作用域描述的是这句话的过程,而这句话就是这个过程的结论。


终于,经过了重重的铺垫,我们要讨论闭包了。

什么是闭包?

闭包 = 函数 + 环境 当然这其中的函数可不是单纯的函数,环境也不是单纯的环境。下面讲一一为你解密,这里的函数到底是啥?环境到底是啥?

极为重要的一点是:每个函数都有与之相关联的环境,而这个环境中包含了其作用域内所有的局部变量,也就是说所有的局部变量都存储在环境中,且这个环境因函数被创建而出现。

解密这个函数时,我们来说说啥叫自由变量?

自由变量:是指不是在本地作用域内定义的变量。 下面举个栗子:

function Say(phrase) {
  var ending = "";
  if (beingFunny) {
    ending = "ok";
  } else if (notSoMuch) {
    ending = " so muck ";
  }
  alert(phrase + ending);
}

看看上面的栗子,先考考你是否能指出哪些是自由变量(一定要根据概念来找哦~)?

​ 很显然,根据概念,beingFunnynotSoMuch都是自由变量,因为在函数 Say 中,它们都没有被定义呀,说明它们来自于其他函数啊,强调一点:概念中的本地作用域是相对于每个函数而言,而不是哪个特定的函数。因此当代码中有多个函数时,我们说的本地作用域是每一个函数的本地作用域。希望这对于你们理解自由变量有帮助吧…

是时候说明函数和环境了

​ 前面我们不是说到过每个函数都有与之相关联的环境吗?环境也是相对于每个函数而言,一个函数有一个与之相关联的环境。因此,假如一个函数中出现了自由变量,并给这个自由变量赋了值,那么环境中是不是也相应地存储了这个有值的自由变量?答案是肯定的。因此,我们说:在某个环境中,当环境中所有的自由变量都被绑定了值(或者说都被赋了值),便将函数敲定了,那么这个函数就叫做敲定函数显然地,环境得是给所有的自由变量绑定了值的环境才行。

综上所述:敲定了的函数 + 给每个自由变量提供了值的环境 === 闭包;

官方说明的闭包概念:包含自由变量的函数与为所有这些自由变量提供了变量绑定的环境一起,被称为闭包。

举个栗子:使用闭包来实现神奇的计数器

function makeCounter() {
  var count = 0;
  function counter() {
    count = count + 1;
    return count;
  }
  return counter;
}
var doCount = makeCounter();
console.log(doCount());
console.log(doCount());
console.log(doCount());

这里哪有闭包呢?我怎么看不出来?

别急,我们一步步来分析闭包到底在哪里。

首先,我们观察到:在函数 counter 外,定义了一个变量 count,而这个变量也在函数内出现了,并进行赋值运算,那么此时我们想想自由变量的定义以及闭包的定义,是不是觉得有点熟悉?

没错!相对于函数 counter 来说,count 就是一个自由变量,而在与函数 counter 相关联的环境中,又为它进行了赋值,所以这个函数 counter 就是一个闭包呀!因此,当函数makeCounter被调用时,返回的是一个闭包。

那么闭包到底有什么好处呢?

在上面那个例子中,使用闭包中的变量 count,在全局作用域中,根本看不到。只能是通过调用闭包或者包含闭包的函数来使用这个变量。

再举一个例子来谈谈闭包的好处以及闭包包含的环境

function setmessage, n){
     setTimeout(function () {
                    alert(message);
          }, n);
     message = "Oh!";
}
set("done!",1000);

显然地,调用 set 时,创建了一个闭包(setTimeout函数那一块),自由变量是 message,而 message 的值最开始为传入的形参”done!”。但是在闭包外,将 message 的值改变了,因此闭包被调用时,将使用的是改变的值。所以,闭包包含的是实际环境,而非环境的副本。

闭包的另一个好处就是:闭包中包含的变量不会在某一事件完成后就消失了,而是一直存在,除非你关闭网页。因此在大多数事件处理程序中,都尽量用闭包,这避免在用户使用中某一事件突然失效而导致用户体验不好。

总之,闭包是个好东西,在事件成立程序中尽量用就对了,以上就是我的思考与体会,希望对你有用,加油!