解释JavaScript中的闭包

去年我写了一篇“closures的简介”,它的目的是帮助大家理解‘什么是闭包,闭包是如何工作的’。现在我尝试从另外一个不同的角度去阐释闭包。有了这些基本的概念,你只需要尽可能多地阅读这些解释,来更全面地理解闭包。

First-class functions

就像我在“Why JavaScript is AWESOME”中解释的那样,JavaScript的强大之处的一部分来自于它的’first-class functions‘。那么编程语言中的’first-class‘意味着什么?

可以被存放在变量和数据结构中

可以作为子例程的参数被传递

可以作为子例程的返回值被返回

可以在运行时被构造

有固有的id(区别于任何给定的名字)

所以,JavaScript中的functions非常类似objects。事实上,在JavaScript中functions就是objects。能够嵌套使用函数,让我们可以使用闭包,这也是我接下来要讨论的...

Nested functions(嵌套函数)

如下是嵌套函数的一个小例子:

http://jsfiddle.net/skilldrick/66jFm/embedded/

在这儿要注意的重要事情是只有唯一的一个f函数被定义。每次函数f被调用后,一个新的函数g被创建,(函数g)局部于函数f的执行过程中。当函数g被返回时,我们可以把它赋值给一个全局变量。所以,我们可以调用f函数,把结果赋值给变量g5;接着我们再一次调用f函数,并把结果赋值给变量g1。g1和g5是2个不同的函数,但碰巧的是它们共享着同一份代码,只不过它们他们在不同的上下文中被执行,使用着不同的‘free variable(自由变量)’。(题外话,使用函数定义去定义‘函数g’,接着返回函数g,并不是必需的。我们可以使用函数表达式作为替代,函数表达式允许我们创建函数时不用命名函数。也就是直接返回匿名函数。这儿是使用匿名函数替换后的版本)

Free variables and scope(自由变量和作用域)

如果一个变量在包含它的作用域中被定义,那么该变量在包含它的作用域内的任何其它作用域内都是自由的(原文:A variable is free in any particular scope if it is defined within an enclosing scope.)。为了表达的更具体,也就是:在函数g的作用域内,变量x是自由的。因为变量x是在函数f的作用域内被定义的(而且‘作用域f’包含‘作用域g’)。同样地,任何全局变量在‘作用域f’和’作用域g‘内也是自由的。

作用域意味着什么?一个作用域是一个代码区,在该代码区中可以定义变量,并且包围该作用域的外围作用域不能访问该作用域内的变量(原文:A scope is an area of code where a variable may be defined, without the enclosing scope knowing about it.)。JavaScript有‘函数作用域’,所以函数有它自己的作用域。所以在‘函数f’中定义的任何变量,外部都是看不到的。作用域是可以嵌套的,所以,在上述例子中,函数g有它自己的作用域,函数g的作用域被函数f包围着,函数f的作用域被全局作用域包围着。当一个变量被访问时,JavaScript解释器在当前作用域内查找变量,如果在当前作用域内找不到该变量的定义,解释器会查看包围着当前作用域的作用域,接着是查看爷爷作用域,一直向上直到全局作用域。如果此时,变量的定义仍未被找到,一个ReferenceError异常就会被抛出。

Closures are functions that retain a reference to their free variables(闭包是‘保留着它们的自由变量的一份引用’的函数)

并且这是问题的本质。让我们先看下上述例子的一个简化版本:

http://jsfiddle.net/skilldrick/XDrsn/embedded/

调用函数f时传递一个参数5,执行函数f时,函数g会被调用。当函数g被调用时,函数g可以访问那个形参x,这并没有什么奇怪的。令人惊讶的地方在于,当你从函数f中返回函数g后,返回的函数g在被调用时仍然可以访问你传递的参数5(就像原先那个例子中展示的那样)。让人迷惑的地方在于:函数g被返回后,仍然记得在函数f被调用时被定义的变量x(这也是大家理解闭包时,有困惑的地方)。从这点来说,确实不能理解。那么看看下面的例子:

http://jsfiddle.net/skilldrick/DEUdF/embedded/

当person被调用,形参name一定是被传递的值。所以,person第一次被调用时,name一定是‘Dave’,person第二次被调用,name一定是‘Mary’。person定义了2个内部函数‘set和get’。当这些函数(set和get)第一次被定义时,它们有一个‘自由变量name’,并且name的值一定是’Dave‘。这2个函数被数组包裹着返回,在外部被取出并赋值给2个变量’getDave和setDave‘。(如果你想从函数中返回一个以上的值,你要么返回一个对象,要么返回一个数组。在这里使用数组显得有点啰嗦,但是如果使用对象的话会混淆我们讨论的问题。这儿又一个使用对象的版本,如果它对你来说更好理解的话)

并且这就是神奇的地方,getDave和setDave都记得同一个变量name(name初始时一定是Dave)。当setDave(’Bob‘)被调用时,变量name被设置为’Bob‘。现在getDave被调用,它返回了’Bob‘。所以getDave和setDave这两个函数记得同一个变量。这也就是我想表达的含义:’闭包是保留它们自由变量的一份引用的函数‘。getDave和setDave都记得它们共有的自由变量name。即使person已经返回,但是变量name继续存活,因为变量name被getDave和setDave引用着。

当person第一次被调用时,变量name一定是’Dave‘。当person第二次被调用时,变量name的一份新版本被创建,当然get和set也被新建了一份。所以getMary,setMary函数和getDave,setDave是完全不同的。虽然它们执行着同样的代码,但是它们的上下文环境不同,有着不同的自由变量。

Summary总结

总的来说,闭包是一个函数’该函数在一个上下文中被调用,(该函数)却记得在另一个上下文中定义的变量‘(也就是该函数被定义的上下文)。在同一个上下文中定义的多个闭包记得同样的上下文,所以任何一个闭包修改上下文,其他闭包也会受影响(因为多个闭包共享同一个上下文,就像上面例子显示的那样 setDave('Bob')后 getDave()也会受到影响)。









本文翻译自:http://skilldrick.co.uk/2011/04/closures-explained-with-javascript/








转载请注明出处








你可能感兴趣的:(解释JavaScript中的闭包)