JavaScript示例问题:词法作用域/闭包 - 精通JavaScript

15

我是一个新手程序员,正在尝试通过《Eloquent JavaScript》学习JS。

到目前为止一切都很好,直到我看到了以下代码示例:

function makeAddFunction(amount) {
  function add(number) {
    return number + amount;
  }
  return add;
}

var addTwo = makeAddFunction(2);
var addFive = makeAddFunction(5);
show(addTwo(1) + addFive(1));

注意:show类似于alert,只是它在JS控制台的屏幕上显示变量,教程已整合。

作者说这是一个例子,展示了词法作用域如何允许合成函数。 章节在此

我不明白的是,addTwoaddFive被称为变量,怎么能将参数发送到makeAddFunctionadd函数中,更具体地说,函数add如何知道变量发送的参数是number参数。

谢谢大家的帮助!


这个链接可能值得一读:http://en.wikipedia.org/wiki/Closure_(computer_science) - Oliver Charlesworth
6个回答

9
在JavaScript中,函数是一种一等对象,也就是说,它可以被传递、赋值给变量等。变量addTwo和addFive包含函数。这些函数是由“工厂”函数makeAddFunction生成的。
addTwo和addFive所包含的函数带有它们创建时存在的作用域。也就是说,当创建addTwo时,参数"amount"为2。因此,addTwo本质上是以下函数:
function addTwo(number) {
   return number + 2;
}

当有人调用addTwo()函数时,它并不会将任何东西传回makeAddFunction函数。makeAddFunction函数已经运行并结束了。但是,在makeAddFunction函数中创建的作用域(其中“amount”等于2)在addTwo函数中仍然存在。

太棒了!我曾经对这个例子(不是你的,是《JavaScript精解》中的一个)感到眼花缭乱、口水横流,但是你的解释让它变得清晰易懂!你的解释是我看到(并理解)的第一个提到当“函数生成器”构建返回的内部函数时,amount被“烘焙”进去的事实。谢谢!!! - mbm29414
我确实需要花几分钟来琢磨它,才能理解这是最清晰的解释。它帮助我将makeAddFunction()看作一个构造函数,因为它返回add()的新实例。 - user110857

8

addTwoaddFive 是变量 -- 但它们是函数变量。看一下 typeof(addTwo) -- 它是一个函数。就好像你做了这样的事情:

var addTwo = function(x) { return x + 2; };

这与此相同:

function addTwo(x) { return x + 2; }

(编辑:正如Šime指出的那样,它们并不完全相同。有关两者之间差异的解释,请参见这里。)

一旦你理解了这一点,例子就会变得容易理解。你甚至可以做一些奇怪的事情,比如声明一个匿名函数并立即调用它:

var seven = function(x) { return x + 2; }(5);

这在物理机器代码级别上,实际上与以下内容完全相同: 对于与此问题相关的所有目的而言,这是等效的:

function addTwo(x) { return x + 2; }
var seven = addTwo(5);

编辑:

也许更为清晰的“前传”是以下内容:

function makeTheAddTwoFunction()
{
    return function(x) { return x + 2; }
}

var addTwo = makeTheAddTwoFunction();

这很傻,但用来说明制作函数的函数。当然,这种类型的函数通常会接受参数,以便每次都能制作不同的函数,但是就是这样。

4
@Ian首先两行代码并不完全等同,它们只是几乎相同。 - Šime Vidas
@Šime 你能解释一下它们之间的区别吗?两者都在当前作用域中定义了符号 addTwo 并将其值设置为相同的函数。我一直把它们视为可互换的,也许这对我不利。 - Ian Henry
抱歉,所给的例子并不相同。你正在将变量的赋值与给函数命名进行比较。它们是不同的!它们可能对this有不同的值;分配给变量的函数可以被重新分配,但函数则不太可能。请记住,这个SO Q是关于语言的细节问题,所以这些问题很重要。 - Larry K
3
@Ian 第一个是VariableStatement(变量声明语句),另一个是FunctionDeclaration(函数声明,不是语句)。 函数声明会在当前执行上下文中的任何其他语句之前进行求值。这意味着通过声明定义的函数可以在调用发生在声明之前的情况下执行(在代码中),如下所示:foo(); function foo() {}。但这不能使用函数变量来实现:foo(); var foo = function() {};(会产生错误)。 - Šime Vidas
@Ian 我认为Šime的意思是,“function name() {}”会将声明和定义都提升到作用域的顶部,而“var name = function () {}”只会提升声明,但不会提升赋值。此外,“function name() {}”会设置函数的名称属性,而“var anonymous = function () {}”则不会。 - Angiosperm
@Šime 谢谢,我已经编辑了帖子解释了区别。@Larry 我不太确定你的意思。函数可以像变量一样被重新分配,而this的值仅取决于它如何被调用--这两者的相同调用不会为this提供不同的值,对吧? - Ian Henry

6

我认为理解这个例子的关键是要知道函数可以返回其他函数(就像任何其他变量一样)。对该代码进行文档记录将有助于更好地理解这个概念。

/** 
 * Creates an adder function
 * @param {number} amount Amount to add
 * @return {function}  Method that adds 'amount' to its argument. 
 * See the documentation of add for its signature
 */
function makeAddFunction(amount) {      
  /**
   * Everytime makeAddFunction is called, a new instance of add  is created
   * (and returned) that holds on to its copy of 'amount' (through the closure)
   * @param {number} number value to add to 'amount'
   * @return {number} 'amount' + 'number'
   */
  return function add(number) {
    return number + amount;
  };
}

// addTwo now is a reference to a function that when called
// adds 2 to whatever is passed in
var addTwo = makeAddFunction(2);
// addFive Adds 5 to its argument
var addFive = makeAddFunction(5);
// addTwo(1) = 3, addFive(1) = 6, therefore, output is 9
show(addTwo(1) + addFive(1));

3

回复:我不明白的是,变量addTwo和addFive如何将参数发送到makeAddFunction函数中?

addTwo和addFive是变量。但它们的值不是简单的标量(数字、字符串等),而是函数。由于它们的值是函数,因此调用这些函数是可以的。例如:addTwo(1)

回复:更具体地说,函数add如何知道变量发送的参数是参数number?

函数add将其第一个参数称为number。 因此,稍后当您通过变量调用该函数时,例如addOne,则传递给addOne的第一个参数变为number。

附注:如果你对自己说,“自己,这很棘手!” 那么你是正确的——这个例子的整个目的就是展示一些棘手的东西。您使用这种技术的频率可能从从不到偶尔使用不等。


0

想象一段代码的最好方法是在脑海中替换值并解释

// when this one is invoked
var addTwo = makeAddFunction(2);

// makeAddFunction
// becomes something like
function makeAddFunction(2) {
  function add(number) {
    return number + 2;
  }
  // return a function, that adds
  // 2 to every number it gets
  return add;
}

// therefore this function call
// will add 2 to 1 and return 3
addTwo(1);

0

一如既往,这里有Jibbring关于JavaScript闭包的笔记。它讨论了作用域、执行上下文、变量/属性解析等等...

虽然JavaScript闭包是词法的,但执行上下文(可能包含属性)是被绑定的(类似Python或Ruby),而不仅仅是“自由变量”(就像C#或Scala一样)。这就是为什么只能在新的函数作用域中引入新的变量的原因。(现代Mozilla实现引入了let)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接