JavaScript闭包和副作用的简明解释? JavaScript中闭包和副作用的简明解释。

32

我一直在阅读一些 JavaScript 的书籍,经常听到闭包和副作用这些术语。但我不明白它们究竟是什么意思。能否有人用简单易懂的英语给我讲解并举例说明呢?(就像向一个平面设计师解释一样)


请澄清一下:您是要求解释闭包和副作用的分别说明,还是两者的结合说明? - user395760
@delnan 抱歉,我修改了标题。 - alexchenco
2
然后请查看https://dev59.com/6XVD5IYBdhLWcg3wBm5h并删除闭包部分,这将是一个重复的问题。副作用也不是什么新鲜事,但我不知道SO上是否有任何具体内容,所以暂时不会投票关闭。 - user395760
这是两个问题,一个关于副作用,另一个关于闭包,应该分开处理。正如delnan所指出的那样,闭包已经被讨论过了,因此最好将这个问题与副作用有关,并参考其他问题来解释闭包。 - outis
6个回答

45

副作用是一个更容易理解的概念。“纯函数”是一种将其输入值映射到输出值的函数 function plus(x, y) { return x + y; }。“副作用”指的是除了返回值以外的任何效果。因此,例如:

function plusWithSideEffects(x, y) {
  alert('This is a side effect'); 
  return x + y;
} 

该函数具有引发警告对话框(并需要用户交互)的副作用。每个代码函数都有一些副作用(如果没有其他副作用,它们都会消耗内存和时间),但是当人们谈论副作用时,他们通常最关心的是IO(如上面的警告对话框)或写入超出函数执行期间的状态。

副作用的挑战在于它们使得函数更难以推理和重复使用。(尽可能接近“纯函数”的函数更容易推理和重复使用,因为它们往往“做一件好事”)。


2
对于“一个平面设计师的编程水平”,这是一个真实的答案。 - Larry OBrien
我不会给这个答案点踩,因为它确实回答了问题,但是我不喜欢在示例中使用 def 关键字,因为它不是 JavaScript 保留字,而是 Python 的东西,对吧? - zappa
@zappa,“def”不是特定于Python的,而且该示例在Python中无法工作,因为它具有大括号而不是正确的缩进。这更像是伪代码甚至是数学符号来展示什么是纯函数。 - Haralan Dobrev
@Haralan Dobrev,啊,谢谢你为我澄清这个问题。 - zappa
这里真的需要使用def吗?这是一个特定于Javascript的问题,代码其他部分完全使用Javascript。除了def之外没有任何伪代码。只需将其更改为function即可... - D G
@DG,我完全同意你的观点。我已经在建议的编辑中将“def”更改为“function”。 - Jesper

9
具有副作用的函数除了返回值之外还会执行其他操作。如果你可以将给定参数的所有函数调用替换为这些参数的值,并且程序的行为保持不变,则没有副作用。这要求函数总是针对给定的参数返回相同的值。
也就是说,假设f(1,2) == 12,如果你可以始终用12替换f(1,2),并且程序的行为保持不变,那么对于这些参数,f就没有副作用。另一方面,如果在一个地方f(1,2) == 12,在另一个地方f(1,2) == 13,则f具有副作用。类似地,如果在用12替换f(1,2)后程序停止发送电子邮件,则f具有副作用。通常,如果f(x,y) == z(其中z取决于x和y),并且你总是可以用z替换每个f(x,y)调用,则f没有副作用。
一些具有副作用的简单函数:
// doesn't always return the same value
function counter() {
    // globals are bad
    return ++x;
}
// omitting calls to `say` change logging behavior
function say(x) {
    console.log(x);
    return x;
}

6

副作用:

将副作用视为同时完成两件事情的内容。 例如:

典型的副作用示例:

var i = 1;
var j = i++;

副作用发生在i++。这里发生的是j变成了1然后i被递增并变成了2。换句话说,发生了两件事情,副作用就是i变成了2。
闭包:
想象一个链式结构像这样: <><><><><><><>。 假设这个链式结构的名字叫做作用域链(scope chain)。然后想象这些链接将对象(objects)连接在一起,如下: <>object<>object<>object<>。 现在,牢记以下内容:
(1) 所有的作用域链开始于全局对象(global object)
(2) 当一个函数被定义时,为其创建一个作用域链并将其存储下来
(3) 当一个函数被调用时,它会创建一个新的对象并将其添加到作用域链中。 现在,请看下面的例子:
function counter () { // define counter
                   var count = 0;
                   return function () { return count + 1;}; // define anonymous function
                   };
var count = counter(); // invoke counter

在这个例子中,当定义了counter()函数时,它的作用域链看起来像这样: <>全局对象<>。然后,在调用counter()函数时,其作用域链看起来像这样: <>全局对象<>计数器对象<>。之后,在counter内部定义和调用了一个没有名称的函数(称为匿名函数)。一旦被调用,匿名函数的作用域链如下所示:<>全局对象<>计数器对象<>匿名函数对象<>。
这里是闭包的部分。如果你注意到,匿名函数使用在它外部定义的变量count。原因是匿名函数可以访问其存储的作用域链中定义的任何变量。这就是闭包的含义,即一个函数以及对其存储的作用域链中任何变量的引用。
然而,在上面的例子中,一旦函数返回,调用时创建的对象就会被丢弃,所以实际上并没有意义。现在看看下面的例子:
function counter () { // define counter
                   var count = 0;
                   function f() { return count + 1;}; // define f
                   return f; // return f
                   };
var count = counter(); // invoke counter

在这个例子中,我返回一个名为f的函数,并将其赋值给变量count。现在变量count持有整个作用域链的引用,它不会被丢弃。换句话说,变量count存储了作用域链,如下所示:<>全局对象<>计数器对象<>匿名函数对象<>。这就是闭包的威力,您可以持有作用域链的引用,并像这样调用它:count()

2
这个答案中的闭包部分是不正确的。JavaScript 具有静态作用域,这意味着函数的作用域链在函数定义时被定义,而不是在函数调用时。该示例有效是因为匿名函数在每次调用 counter 时都被定义,这意味着每个返回的函数将具有一个不同的激活对象用于 counter 的作用域链,因此具有不同的局部变量集合(例如 count)。更多信息请参见此答案 - josh3736
@josh3736:感谢您的评论。我已更改我的答案,请指出任何错误。另外,我想评论一下,对于全局函数声明,该函数将仅具有对包含全局对象的作用域链的引用。只有在调用它之后,它才会具有对激活对象的引用。 - Jesse Good

0

主要的副作用是函数内部与外部世界的交互。一些副作用的例子包括: API调用或HTTP请求,数据突变,输出到屏幕或控制台, DOM查询/操作。 示例:

var a = 12
function addTwo(){
   a = a + 2; // side-effect

}
addTwo()

闭包

根据MDN的说法,

闭包允许内部函数访问外部函数的作用域。在JavaScript中,每次创建函数时都会创建闭包。

示例:

function outer(){
  var a = 12; // Declared in outer function 
  function addTwo(){ // closure function
    a = a + 2; // acessing outer function property
    console.log(a)
  }
  addTwo();
 }
 outer()


0

我是JavaScript的新手,不会谈论闭包。然而,我的JavaScript新手状态让我非常关注在我的通常编程语言(Erlang)中不可能使用的副作用。

副作用似乎是JavaScript中改变状态的一种常见方式。例如,从w3cschools.com网站上看到的这个例子:

<script>
function myFunction() {
    document.getElementById("demo").innerHTML = "Paragraph changed.";
}
</script>

这里没有输入参数或返回值,而是文档的内容会因为其在函数中具有全局作用域而被更改。例如,如果你要用Erlang编写这个函数,文档将作为参数传入,并返回新的文档状态。调用程序的人会看到一个文档被传入并返回一个已更改的文档。

如果看到函数没有明确返回新状态,那么程序员应该警惕可能存在副作用的使用。


0

例子

function outer() {
    var outerVar;

    var func = function() {
        var innerVar
        ...
        x = innerVar + outerVar
    }
    return func
}

当 outer() 结束时,函数 func() 继续存在,并且使用实用。


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