什么是JavaScript闭包?

简介: 什么是JavaScript闭包? 本文转载自:众成翻译 译者:Mcbai 链接:http://www.zcfy.cc/article/4639 原文:https://medium.freecodecamp.org/whats-a-javascript-closure-in-plain-english-please-6a1fc1d2ff1cJavaScript闭包就如同汽车的功能——不同的位置都有对应那辆车的不同组件。

什么是JavaScript闭包?

本文转载自:众成翻译
译者:Mcbai
链接:http://www.zcfy.cc/article/4639
原文:https://medium.freecodecamp.org/whats-a-javascript-closure-in-plain-english-please-6a1fc1d2ff1c

JavaScript闭包就如同汽车的功能——不同的位置都有对应那辆车的不同组件。

JavaScript中的每一个函数都构成一个闭包,这也是JavaScript最酷的特点之一。因为没有闭包的话,实现像回调函数或者事件句柄这样的公共结构就会很困难。

不管你什么时候定义了一个函数,你都创建了一个闭包。然后当你执行这些函数时,他们的闭包能够让他们访问他们作用域内的数据。

这有点像生产一辆带有一些像start, accelerate, decelerate之类功能的汽车。司机每次操纵他们的车时执行这些功能。定义这些函数的闭包就像汽车一样,并且‘闭合’了需要操作的变量。

让我们拿accelerate函数做一个简单的类比,当汽车被制造的时候,函数也就被定义了:

function accelerate(force) {
  // Is the car started?
  // Do we have fuel?
  // Are we in traction control mode?
  // Many other checks...
  // If all good, burn more fuel depending on 
  // the force variable (how hard we’re pressing the gas pedal)
}

每次司机踩下油门,这个方法就被执行。注意这个函数需要访问很多变量才能执行,包括它自己的force变量。但是更重要的是,它需要自己作用域外被其它汽车功能控制的变量。这就是accelerate函数的闭包(我们从汽车本身获得到的)的用处。

以下是accelerate函数的闭包对加速函数所作出的承诺:

> 好的accelerate,当你执行时,你可以访问你的_force_变量,你可以访问_isCarStarted_变量,也可以访问_fuelLevel_变量和_isTractionControlOn_变量。 你也可以控制我们发送给引擎的_currentFuelSupply_变量。

请注意,闭包不会为这些变量赋予acceleration函数确切的值,而是允许在accelerate函数执行时访问这些值。

闭包与函数作用域密切相关,因此理解这些作用域如何工作将有助于理解闭包。 简而言之,了解作用域最重要的就是了解当你执行一个函数时,一个私有函数作用域被创建并用于执行该函数的过程。

然后当你内部函数开始执行函数时,这些函数作用域就会形成嵌套。

当你定义一个函数时就创建了一个闭包,而不是当你执行它的时候。然后,每当你执行这个函数,其已经定义的闭包使它可以访问所有对它可用的函数作用域。

在某种程度上,你可以认为作用域是临时的(全局作用域除外),而把闭包是永久的。

一个chrome调试工具展示的闭包。

想要真正了解闭包好它在JavaScript里扮演的角色,你首先需要明白几个简单的JavaScript函数和作用域的概念。

在我们开始之前,注意我已经创建了一个交互实验,你可以在这里查看。

1 — 按引用分配函数

当你把一个函数赋值给一个变量,就像这样:

function sayHello() {
  console.log("hello");
};

var func = sayHello;

你正在给变量func赋予一个sayHello的引用,而不是复制。这使得func仅仅是sayHello的一个别名,你在这个别名上做的任何事,其实都是在原来的函数上操作的。比如:

func.answer = 42;

console.log(sayHello.answer); // prints 42

属性的answer是直接在func上设置的,然后使用sayHello进行读取,这依然是有效的。

你还可以通过执行func别名来执行sayHello:

func() // prints "hello"

2 — 作用域有生命周期

当你调用一个函数时,在执行该函数期间创建一个作用域,函数执行完毕,作用域消失。

当你第二次调用该函数时,在第二个执行期间创建一个新的不同的作用域,当函数执行完毕,第二个作用域也随之消失。

function printA() {
  console.log(answer);
  var answer = 1;
};
printA(); // this creates a scope which gets discarded right after
printA(); // this creates a new different scope which also gets discarded right after;

在上面的示例中创建的这两个作用域是不同的。这里的变量answer在它们两个之间完全是不共享的。

每个函数作用域都有一个生命周期。它们会被创建出来,然后又立刻被丢弃。惟一的例外是全局作用域,只要应用程序在运行,它就不会消失。

3 — 闭包跨越多个作用域

当你定义一个函数,也就创建了一个闭包

和作用域不同,闭包是当你定义一个函数时创建的,而不是你执行函数的时候。闭包在你执行完函数后也不会消失。

在定义了一个函数很久以后,你依然可以访问闭包里的数据,即使它执行了也是一样。

一个闭包包含所有定义好的函数可以访问的书。这意味着定义函数的作用域,全局作用域和定义函数作用域之间嵌套的作用域,以及全局作用域本身。

var G = 'G';

// Define a function and create a closure
function functionA() {
  var A = 'A'

  // Define a function and create a closure
  function functionB() {
    var B = 'B'
    console.log(A, B, G);
  }

  functionB();  // prints A, B, G

  // functionB closure does not get discarded
  A = 42;
  functionB();  // prints 42, B, G
}

functionA();

当我们定义一个functionB所创建的闭包,允许我们访问functionB的作用域,functionA的作用域以及全局作用域。

每次我们执行functionB,我们都可以通过先前创建好的闭包访问变量B, A, 和 G。然而,闭包并不是复制了这些变量,而是引用它们。

例如,functionB的闭包被创建之后,变量A的值会在某些时候发生变化,当我们执行functionB之后,我们会看到新的值,而不是旧的值。functionB的第二个调用打印42、B、G,因为变量A的值被更改为42,闭包给我们提供了一个引用,而不是一个副本。

不要将闭包和作用域混淆

把闭包与作用域混淆是很常见的,所以让我们确保不要这样做。

// scope: global
var a = 1;
void function one() {
  // scope: one
  // closure: [one, global]
  var b = 2;

  void function two() {
    // scope: two
    // closure: [two, one, global]
    var c = 3;

    void function three() {
      // scope: three
      // closure: [three, two, one, global]
      var d = 4;
      console.log(a + b + c + d); // prints 10
    }();
  }();
}();

在上面的简单例子中,我们定义并立即调用了三个函数,所以他们都创建了作用域和闭包。

函数one()的作用域就是它自己,它的闭包让我们有访问它和全局作用域的权利。

函数two()的作用域就是它自己,它的闭包让我们有访问它和函数one(),还有全局作用域的权利。

同样,函数three()的闭包给我们访问所有作用域的权力。这就是为什么我们可以在函数three()中访问所有变量的原因。

但是作用域和闭包的关系不总是如此。在不同作用域里定义和调用函数时,情况又会变得不一样。让我通过一个例子来解释:

var v = 1;

var f1 = function () {
  console.log(v);
}

var f2 = function() {
  var v = 2;
  f1(); // Will this print 1 or 2?
};

f2();

你认为上面的例子中会打印1还是2?代码很简单,函数f1()打印v的值,是全局作用域的1。但是我们在有不同的值等于2的v的函数f2()里执行f1(),然后再执行f2()

这段代码将会打印1还是2?

如果你想说2,那么你将会感到惊讶,这段代码实际上会打印1。原因是作用域和闭包并不相同。console.log方法会使用当我们定义f1()时所创建的f1()闭包,这意味着f1()的闭包值允许我们访问f1()和全局的作用域。

我们执行f1()的地方的作用域并不会影响闭包。实际上,f1()的闭包并不会给我们访问函数f2()作用域的权力。如果你删除全局变量v,然后执行这段代码,你将会得到错误消息:

var f1 = function () {
  console.log(v);
}

var f2 = function() {
  var v = 2;
  f1(); // ReferenceError: v is not defined
};

f2();

这对理解和记忆非常重要。

4 — 闭包有读和写的权限

由于闭包给我们提供了在作用域中的变量的引用,所以意味着它们给我们的权限包括读和写,而且不仅仅是读。

看看这个例子:

function outer() {
  let a = 42;

  function inner() {
    a = 43;
  }

  inner();
  console.log(a);
}

outer();

我们定义了一个inner()函数,创建了一个可以让我们访问变量a的闭包。我们可以读写这个变量,并且如我们我们真的改变了它的值,我们会改变outer()作用域里变量a的值。

这段代码会打印43,因为我们用inner()函数的闭包改变了outer()函数的变量

这就是为什么我们可以在任何地方改变全局变量。所有闭包都给我们提供了对所有全局变量的读写权限。

5 — 闭包可以分享作用域

因为在定义函数时,闭包就给我们访问嵌套作用域的权力,所以当我们在同一个作用域中定义多个函数时,这个作用域就被其中的闭包共享。由于这个原因,全局作用域总是被所有闭包共享。

function parent() {
  let a = 10;

  function double() {
    a = a+a;
   console.log(a);
  };

  function square() {
    a = a*a;
   console.log(a);
  }

  return { double, square }
}

let { double, square } = parent();

double(); // prints 20
square(); // prints 400
double(); // prints 800

在上面的例子中,我们有一个设置变量a的值为10的函数parent(),我们在函数parent()的作用域里定义了两个函数,double()square()。定义函数double()square()时所创建的闭包共享函数double() 的作用域。

因为double()square()都会改变变量a,当我们执行最后3行代码时,我们先把a相加(让a = 20),然后把相加后的值相乘(让a = 400),然后把相乘后的值相加(让a = 800)。

最后一个测试

让我们来测试到目前为止你对闭包的理解。在你执行下面的代码之前,先猜猜它会打印什么:

let a = 1;

const function1 = function() {
  console.log(a);
  a = 2
}

a = 3;

const function2 = function() {
  console.log(a);
}

function1();
function2();

我希望得到正确答案并且希望这个简单的概念能帮你正真理解函数闭包在JavaScript里扮演的重要角色。

相关文章
|
18天前
|
自然语言处理 JavaScript 前端开发
JavaScript中闭包:概念、用途与潜在问题
【4月更文挑战第22天】JavaScript中的闭包是函数及其相关词法环境的组合,允许访问外部作用域,常用于数据封装、回调函数和装饰器。然而,不恰当使用可能导致内存泄漏和性能下降。为避免问题,需及时解除引用,减少不必要的闭包,以及优化闭包使用。理解并慎用闭包是关键。
|
1月前
|
JavaScript
闭包(js的问题)
闭包(js的问题)
13 0
|
2月前
|
JavaScript 前端开发
解释JavaScript闭包的工作原理,并举例说明其在游戏开发中的应用。
JavaScript闭包允许内部函数访问并保持对外部函数变量的引用,即使外部函数执行结束。当函数返回内部函数时,形成闭包,继承父函数作用域链。在游戏开发中,闭包用于创建具有独立状态和行为的角色实例。例如,`createCharacter`函数创建角色并返回包含属性和方法的对象,内部函数如`getHealth`、`setHealth`和`attack`通过闭包访问并操作角色的变量。这种方式确保了每个角色的状态在不同实例间独立,是实现游戏逻辑的强大工具。
14 2
|
2月前
|
存储 缓存 JavaScript
|
2月前
|
自然语言处理 JavaScript 前端开发
探索JavaScript中的闭包:理解其原理与实际应用
探索JavaScript中的闭包:理解其原理与实际应用
20 0
|
2月前
|
自然语言处理 JavaScript 前端开发
深入理解JS的执行上下文、词法作用域和闭包(中)
深入理解JS的执行上下文、词法作用域和闭包(中)
|
2月前
|
存储 自然语言处理 JavaScript
深入理解JS的执行上下文、词法作用域和闭包(上)
深入理解JS的执行上下文、词法作用域和闭包(上)
|
3天前
|
前端开发 JavaScript
闭包在JavaScript中有许多应用场景
闭包在JavaScript中发挥关键作用,如封装私有变量和函数提升安全性,维护变量生命周期,实现高阶函数,模拟块级作用域,支持回调函数以处理异步操作,以及促进模块化编程,增强代码组织和管理。闭包是理解和掌握JavaScript高级特性的重要一环。
20 7
|
10天前
|
自然语言处理 JavaScript 前端开发
【JavaScript技术专栏】深入理解JavaScript作用域与闭包
【4月更文挑战第30天】了解JavaScript的关键在于掌握作用域和闭包。作用域决定变量和函数的可访问范围,分为全局(在`window`或`global`对象中)和局部(函数内部)。闭包则允许函数访问其创建时的作用域,即使在其他地方调用。它通过作用域链保存对外部变量的引用,常用于实现私有变量、模块化和柯里化。然而,不当使用闭包可能导致内存泄漏和性能下降。理解这些概念能提升代码质量,但也需谨慎处理潜在问题。
|
11天前
|
自然语言处理 前端开发 JavaScript
【Web 前端】什么是JS闭包?
【4月更文挑战第22天】【Web 前端】什么是JS闭包?