了解JavaScript闭包和作用域链的基本过程

When developers start exploring the JavaScript programming language, the concept of Scope and Closures can be expected to be a hurdle to their progress. The reason behind this behavior is the complexity of the other concepts that lie under the hood of this feature in JavaScript.

当开发人员开始探索JavaScript编程语言时,“范围和闭包”的概念可能会成为其发展的障碍。 此行为背后的原因是JavaScript中此功能背后的其他概念的复杂性。

However, a solid understanding of JavaScript’s Scope and Closures is so important that a good grasp of the concept will significantly augment the developer’s knowledge and prepare him/her for many years of writing elegant code.

但是,对JavaScript的范围和闭包的扎实理解是如此重要,以至于对这一概念的良好掌握将极大地增强开发人员的知识,并为他/她多年编写优雅代码的准备。

In his book: You Don't Know JS: Scope & Closures, Kyle Simpson took the time to go over the concept in detail, treating each underlying logic as a single entity that merits intense study.

凯尔·辛普森Kyle Simpson )在他的《 你不知道JS :范围和闭包》一书中花了一些时间详细研究这个概念,将每个基本逻辑视为一个值得深入研究的单一实体。

为什么我们需要范围? ( Why do we need Scopes? )

For us to see the need for a scoping mechanism, we will use variables as a case study. The ability to store values in variables, and later retrieve and modify those values is a fundamental property of virtually every programming language.

为了让我们看到需要范围界定机制,我们将使用变量作为案例研究。 将值存储在变量中,以及以后检索和修改这些值的能力实际上是每种编程语言的基本属性。

The variable themselves need to be stored in a way that makes it easy for them to be found during run-time, and it is this idea that suggests the need for a scoping mechanism.

变量本身需要以一种易于存储的方式存储,正是这种想法表明需要范围界定机制。

The Scope is a well-defined set of rules for storing variables in a location and finding those variables at a later time. It is a lookup list of all the declared identifiers (variables), and it enforces a strict set of rules that determine how these variables are accessed during code execution.

范围是一组定义明确的规则,用于将变量存储在某个位置并在以后查找这些变量。 它是所有已声明的标识符(变量)的查找列表,并且执行一组严格的规则,这些规则确定在代码执行期间如何访问这些变量。

JavaScript is generally categorized as a dynamic or interpreted language. However, it is, in fact, a compiled language. The subtle difference here is that it is neither compiled in advance as the conventional compiled languages are, nor are the results of its compilation immediately portable across various distributed systems.

JavaScript通常被分类为动态语言或解释语言。 但是,实际上,它是一种编译语言。 此处的细微差别是,它既没有像常规编译语言那样预先编译,也没有立即在各种分布式系统之间移植其编译结果。

What happens in the case of JavaScript is: just before execution, tokenizing/lexing and parsing are carried out by the compiler. However, the process of generating executable code is carried out in a unique manner.

就JavaScript而言,发生的事情是:在执行之前,编译器将执行tokenizing/lexing分析和解析。 但是,生成可执行代码的过程是以独特的方式执行的。

Let's consider this variable declaration snippet:

让我们考虑以下变量声明代码段:

var a;

Behind the scenes, the compiler checks that particular scope collection for a variable with the identifier a. If a already exists within the scope, the compiler ignores the declaration and moves on. Otherwise, the compiler declares a new variable with the identifier a for that scope collection.

在幕后,编译器检查该特定范围集合与所述标识符的变量a 。 如果a已范围内存在,编译器会忽略对宣言和行动。 否则,编译器用该范围集合的标识符a声明一个新变量。

Next, the compiler generates the code for the JavaScript engine — responsible for start-to-finish compilation and execution of the JavaScript program — to execute.

接下来,编译器为JavaScript引擎生成代码,该代码负责JavaScript程序的从头到尾的编译和执行。

Let's consider this variable assignment snippet:

让我们考虑一下这个变量赋值片段:

a= 2;

Let's Imagine that this code is under the scope collection of the first snippet we looked at. For the compiler to execute this code, it first checks that the variable a is accessible within the current scope. If it is accessible, the compiler uses that variable and performs the assignment instruction, if it isn't accessible, the compiler traverses the scope chain (we will discuss nested scopes in the next session) until it finds a scope where a exists and is accessible.

让我们想象一下,此代码在我们查看的第一个代码段的作用域集合内。 为了使编译器执行此代码,它首先检查变量a在当前作用域内是否可访问。 如果是访问,编译器将使用该变量并执行任务指令,如果它是无法访问的,编译器遍历作用域链(我们将在下次会议讨论嵌套范围),直到找到一个范围,其中a存在且无障碍。

For more information on how the JavaScript engine checks the scope and handles look up, you can read Kyle Simpson’s book here.

有关JavaScript引擎如何检查范围和处理查找的更多信息,您可以在此处阅读Kyle Simpson的书。

We will now look at different kind of scopes.

现在,我们将讨论不同类型的范围。

嵌套范围 ( Nested Scope )

As the name implies, the nested scope refers to a scope that is ‘placed’ within another scope. This is usually possible with functions.

顾名思义,嵌套范围是指“放置”在另一个范围内的范围。 这通常可以通过功能实现。

Let’s consider this example:

让我们考虑这个例子:

function foo(a) {
    console.log(a * b);
   }
  var b = 3;
  foo(1);//3

When we run this program, we notice that the value of b cannot be resolved inside the foo function but it can be resolved in the Scope surrounding it.

当运行该程序时,我们注意到b的值无法在foo函数中解析,但可以在其周围的Scope中解析。

词汇范围 ( Lexical Scope )

The lexical scope refers to where variables and blocks are authored by the programmer at write time, this is (usually) set in stone when the lexer (handles the tokenizing/lexing phase of compilation) processes the code.

词法作用域指的是程序员在写时在哪里编写变量和块,这通常在词法分析器(处理编译的tokenizing/lexing阶段)处理代码时固定在石头上。

Let’s consider this block of code:

让我们考虑以下代码块:

function foo(a) {
  var b = a + 2;
  function bar(c) {
    console.log(a, b, c);
}
  bar(b * 2);
}

foo(3); // 3, 5, 10

There are three scopes present in this example:

本示例中存在三个范围:

  • the global scope, which has just one identifier in it - foo

    全局范围,其中只有一个标识符foo
  • the scope of foo, which includes the three identifiers - a, bar and b.

    的范围foo ,它包括三个标识符- abarb
  • the scope of bar, and it includes just one identifier - c

    bar的范围,它仅包含一个标识符c

功能和范围 ( Function and Block Scope )

Function scope supports the idea that all variables belong to a function and can be re-used throughout the lifetime of a function (the variables are even accessible to the nested scopes).

函数作用域支持所有变量都属于一个函数并且可以在函数的整个生命周期内重复使用的想法(嵌套作用域甚至可以访问这些变量)。

Let’s consider this code snippet, the scope of foo(..) includes identifiers a, b, c and bar:

让我们考虑一下此代码段, foo(..)的范围包括标识符a, b, cbar

function foo(a) {
      var b = 2;
      //some code

      function bar() {
        //...
      } 
      //more code
      var c = 3;
    }

It doesn’t matter where in the scope a declaration appears, the variable or function belongs to the containing scope, regardless. All identifiers directly inside foo(..) are accessible within its scope, and also available within bar(..) (assuming there are no shadow identifier declarations inside of bar(..)).

声明在范围中出现的位置与变量或函数都属于包含的范围无关,无论如何。 直接在foo(..)内部的所有标识符都可以在其范围内访问,并且也可以在bar(..) (假定bar(..)内部没有阴影标识符声明)。

This design approach is really useful since it utilizes JavaScript's dynamic nature that enables it to take on values of different types as needed. However, without careful precautions, variables existing across the entirety of a scope can lead to some unexpected pitfalls.

这种设计方法非常有用,因为它利用了JavaScript的动态特性,使它能够根据需要采用不同类型的值。 但是,如果没有仔细的预防措施,整个示波器中存在的变量可能会导致一些意外的陷阱。

Block scoping, on the other hand, refers to the idea that the variables within a particular block in a program are under the block’s scope. A block is JavaScript is usually a {..} pair.

另一方面,块作用域定义是指程序中特定块内的变量在该块范围内的想法。 JavaScript的一个块通常是一对{..}

Before the advent of ES6 in 2015, there was a major issue with block scoping in JavaScript. This issue was due to the fact that the scope in which a variable was declared (using the var keyword) didn’t matter, because it always belonged to the enclosing scope.

在2015年ES6问世之前,JavaScript的块范围界定存在一个主要问题。 此问题是由于以下事实导致的:声明变量(使用var关键字)的范围并不重要,因为它始终属于封闭的范围。

Consider the example below:

考虑下面的示例:

var foo = true;

if (foo) {
  var bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

This snippet is essentially categorized as ‘fake’ block-scoping because the variable bar is still accessible outside the if statement. A workaround to this behavior would be declaring variables as close as possible, as local as possible, to where they will be used. Among the several block-scoping structures in JavaScript, which include: with, try/catch, let, const and garbage collection, we will focus on let and const which were introduced with ES6 as two ways of enforcing block-scoping.

该代码段实质上被归类为“伪”块作用域,因为在if语句之外仍然可以访问变量bar 。 解决此问题的一种方法是声明变量,变量应尽可能靠近变量所在的地方。 在JavaScript的几种块作用域结构中,包括: withtry/catchletconst和垃圾回收,我们将重点介绍ES6中引入的letconst作为执行块作用域的两种方式。

The let keyword attaches the variable declaration to the scope of whatever block (usually a {..} pair) it is contained in:

let关键字将变量声明附加到包含在其中的任何块(通常是{..}对)的范围内:

var foo = true;

if (foo) {
  let bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

console.log(bar);//ReferenceError

Using let to attach a variable to an existing block is somewhat implicit. Hence attention should be paid to which blocks have their variables scoped to them as programmers develop and evolve their code. The creation of explicit blocks can tackle some of these issues, making it obvious where variables are attached and where they are not. This is preferable as it is easier to achieve, and fits more naturally with how block-scoping works in other languages:

使用let将变量附加到现有块有些隐含。 因此,在程序员开发和发展代码时,应注意哪些块的变量范围受其限制。 显式块的创建可以解决其中的一些问题,从而使变量在何处附加和不附加在何处变得显而易见。 这是可取的,因为它更容易实现,并且更自然地适合其他语言中的块作用域工作原理:

var foo = true;

if (foo) {
  {//<--explicit block
    let bar = foo * 2;
    bar = something(bar);
    console.log(bar);
  }
}
console.log(bar); //ReferenceError

As shown in the code snippet above, we can create an arbitrary block for let to bind to, by simply including a {..} pair anywhere a statement is a valid grammar. The explicit block inside the if statement will make refactoring easier without affecting the position and semantics of the surrounding if statement. In addition to let, const also creates a block-scoped variable, but whose value is fixed (a constant). An attempt to change this value results in an error:

如上面的代码片段所示,我们可以通过在语句为有效语法的任何地方简单地包含一个{..}对,来创建一个供let绑定的任意块。 if语句中的显式块将使重构更加容易,而不会影响周围if语句的位置和语义。 除了letconst还创建一个块作用域变量,但其值是固定的(常量)。 尝试更改此值将导致错误:

var foo = true;

if (foo) {
  var a = 2;
  const b = 3; //block-scoped to the containing if

  a = 3; //Just fine!
  b = 4; //error!
} 
console.log(a); //3
console.log(b);//ReferenceError

吊装 ( Hoisting )

Consider the code below:

考虑下面的代码:

a= 2;
var a;
console.log(a);

The resulting output is 2, contrary to what is expected by basic programming logic where it should return undefined because the variable was initialized before it was declared.

结果输出为2,这与基本编程逻辑所期望的输出相反,该输出应返回undefined因为变量在声明之前已初始化。

Consider another piece of code:

考虑另一段代码:

console.log(a);
var a = 2;

This code outputs undefined.

此代码输出undefined

The reason for this behavior can be explained by understanding how the JavaScript Engine executes code. The Engine first compiles the code before it interprets it. This means that variables and functions are processed first before any other part of the code is executed. The concept of ‘Hoisting’ refers to the idea that variable and function declarations are “lifted” from where they appear in the code flow to the top of the code.

可以通过了解JavaScript引擎如何执行代码来解释这种现象的原因。 引擎在解释代码之前先对其进行编译。 这意味着在执行代码的任何其他部分之前,先处理变量和函数。 “提升”的概念是指变量和函数声明从它们在代码流中出现的位置“提升”到代码顶部的想法。

It is important to note that function declarations are hoisted before variable declarations are.

重要的是要注意,在变量声明之前先悬挂函数声明。

Consider the code below:

考虑下面的代码:

foo(); //1
var foo;

function foo() {
  console.log(1);
}
foo = function() {
  console.log(2);
};

The resulting output is 1 instead of the expected 2

结果输出为1而不是预期的2

关闭 ( Closures )

A Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope. Let’s look at some code to illustrate this definition:

闭包是指某个函数能够记住并访问其词法范围,即使该函数正在其词法范围之外执行。 让我们看一些代码来说明此定义:

function foo() {
  var a = 2;
  function bar() {
    console.log(a); //2
  }
  bar();
}
foo();

The function bar has a closure over the scope of foo() and other enclosing scopes, such as the global scope in this case. Put slightly differently, it’s said that bar() closes over the scope of foo(). Why? Because bar() appears nested inside of foo(). Let’s look at another example for a better understanding:

功能barfoo()范围和其他封闭范围(例如本例中的全局范围foo()具有闭包。 换句话说,据说bar()关闭了foo()的范围。 为什么? 因为bar()看起来嵌套在foo()内部。 让我们看另一个示例以更好地理解:

function foo(){
  var a = 2;

  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz();//2

The function bar() has lexical scope access to the inner scope of foo(). bar() is passed as a value by returning the function object that bar references. After we execute foo(), we assign the value it returned (the inner bar() function) to a variable called baz, and then we invoke baz(), which is invoking the inner bar() function. This causes bar() to be executed, but in this case, outside its declared lexical scope.

函数bar()具有对foo()的内部范围的词法范围访问。 通过返回bar引用的函数对象,将bar()作为值传递。 执行foo() ,我们将其返回的值(内部bar()函数)分配给名为baz的变量,然后调用baz() ,后者正在调用内部bar()函数。 这将导致执行bar() ,但在这种情况下,将超出其声明的词法范围。

bar has a lexical scope closure over that inner scope of foo() which keeps that scope alive for bar() to reference at any later time. bar() has a reference to that scope, and that reference is called a closure. Therefore, when baz is invoked (invoking the inner function labeled bar), it has access to its author-time lexical scope, so it can access the variable a. Closure makes it possible for a function to continue to access the lexical scope it was defined in at author-time.

barfoo()内部范围上有一个词法作用域闭包,它使该范围保持活动状态,以便bar()在以后的任何时间引用。 bar()具有对该范围的引用,该引用称为闭包。 因此,在调用baz时(调用标记为bar的内部函数),它可以访问其作者时间词法范围,因此可以访问变量a 。 闭合使函数有可能继续访问在作者时定义的词法范围。

结论 ( Conclusion )

We have taken a brief tour over the concept of Closures and Scopes in JavaScript. JavaScript programmers can’t afford to ignore this feature because it defines the process of variable lookup during code execution. Without the strict rules that are defined by the scope, the code we write will produce rather clumsy and unexpected results at run-time. We also looked at the scoping benefits that come with using the let and const keywords. If reading this article has piqued your interest on the topic of Scope and Closures, you can learn more about it here.

我们简要介绍了JavaScript中的Closures和Scopes概念。 JavaScript程序员不能忽略此功能,因为它定义了代码执行过程中变量查找的过程。 没有范围定义的严格规则,我们编写的代码将在运行时产生相当笨拙和意外的结果。 我们还研究了使用letconst关键字带来的范围界定优势。 如果阅读本文引起了您对“范围和闭包”主题的兴趣,则可以在此处了解更多信息

翻译自: https://scotch.io/tutorials/understanding-the-underlying-processes-of-javascripts-closures-and-scope-chain

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值