Deep JavaScript chapter: How do environments and closures work in JavaScript?

栏目: IT技术 · 发布时间: 3年前

内容简介:(Ad, please don’t block.)In this chapter, we take a closer look at how the ECMAScript language specification handles variables.An environment is the data structure that the ECMAScript specification uses to manage variables. It is a dictionary whose keys ar

(Ad, please don’t block.)

4 Environments: under the hood of variables

In this chapter, we take a closer look at how the ECMAScript language specification handles variables.

4.1 Environment: data structure for managing variables

An environment is the data structure that the ECMAScript specification uses to manage variables. It is a dictionary whose keys are variable names and whose values are the values of those variables. Each scope has its associated environment. Environments must be able to support the following phenomena related to variables:

  • Recursion
  • Nested scopes
  • Closures

We’ll use examples to illustrate how that is done for each phenomenon.

4.2 Recursion via environments

We’ll tackle recursion first. Consider the following code:

function f(x) {
  return x * 2;
}
function g(y) {
  const tmp = y + 1;
  return f(tmp);
}
assert.equal(g(3), 8);

For each function call, you need fresh storage space for the variables (parameters and local variables) of the called function. This is managed via a stack of so-called execution contexts , which are references to environments (for the purpose of this chapter). Environments themselves are stored on the heap. That is necessary because they occasionally live on after execution has left their scopes (we’ll see that when exploring closures ). Therefore, they themselves can’t be managed via a stack.

4.2.1 Executing the code

While executing the code, we make the following pauses:

function f(x) {
  // Pause 3
  return x * 2;
}
function g(y) {
  const tmp = y + 1;
  // Pause 2
  return f(tmp);
}
// Pause 1
assert.equal(g(3), 8);

This is what happens:

  • Pause 1 – before calling g() (fig. ).

  • Pause 2 – while executing g() (fig. ).

  • Pause 3 – while executing f() (fig. ).

  • Remaining steps: Every time there is a return , one execution context is removed from the stack.

Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 1: Recursion, pause 1 – before calling g() : The execution context stack has one entry, which points to the top-level environment. In that environment, there are two entries; one for f() and one for g() .
Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 2: Recursion, pause 2 – while executing g() : The top of the execution context stack points to the environment that was created for g() . That environment contains entries for the argument y and for the local variable tmp .
Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 3: Recursion, pause 3 – while executing f() : The top execution context now points to the environment for f() .

4.3 Nested scopes via environments

We use the following code to explore how nested scopes are implemented via environments.

function f(x) {
  function square() {
    const result = x * x;
    return result;
  }
  return square();
}
assert.equal(f(6), 36);

Here, we have three nested scopes: The top-level scope, the scope of f() , and the scope of square() . Observations:

  • The scopes are connected. An inner scope “inherits” all the variables of an outer scope (minus the ones it shadows).
  • Nesting scopes as a mechanism is independent of recursion. The latter is best managed by a stack of independent environments. The former is a relationship that each environment has with the environment “in which” it is created.

Therefore, the environment of each scope points to the environment of the surrounding scope via a field called outer . When we are looking up the value of a variable, we first search for its name in the current environment, then in the outer environment, then in the outer environment’s outer environment, etc. The whole chain of outer environments contains all variables that can currently be accessed (minus shadowed variables).

When you make a function call, you create a new environment. The outer environment of that environment is the environment in which the function was created. To help set up the field outer of environments created via function calls, each function has an internal property named [[Scope]] that points to its “birth environment”.

4.3.1 Executing the code

These are the pauses we are making while executing the code:

function f(x) {
  function square() {
    const result = x * x;
    // Pause 3
    return result;
  }
  // Pause 2
  return square();
}
// Pause 1
assert.equal(f(6), 36);

This is what happens:

  • Pause 1 – before calling f() (fig. ).
  • Pause 2 – while executing f() (fig. ).
  • Pause 3 – while executing square() (fig. ).
  • After that, return statements pop execution entries off the stack.
Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 4: Nested scopes, pause 1 – before calling f() : The top-level environment has a single entry, for f() . The birth environment of f() is the top-level environment. Therefore, f ’s [[Scope]] points to it.
Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 5: Nested scopes, pause 2 – while executing f() : There is now an environment for the function call f(6) . The outer environment of that environment is the birth environment of f() (the top-level environment at index 0). We can see that the field outer was set to the value of f ’s [[Scope]] . Furthermore, the [[Scope]] of the new function square() is the environment that was just created.
Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 6: Nested scopes, pause 3 – while executing square() : The previous pattern was repeated: the outer of the most recent environment was set up via the [[Scope]] of the function that we just called. The chain of scopes created via outer , contains all variables that are active right now. For example, we can access result , square , and f if we want to. Environments reflect two aspects of variables. First, the chain of outer environments reflects the nested static scopes. Second, the stack of execution contexts reflects what function calls were made, dynamically.

4.4 Closures and environments

To see how environments are used to implementclosures, we are using the following example:

function add(x) {
  return (y) => { // (A)
    return x + y;
  };
}
assert.equal(add(3)(1), 4); // (B)

What is going on here? add() is a function that returns a function. When we make the nested function call add(3)(1) in line B, the first parameter is for add() , the second parameter is for the function it returns. This works because the function created in line A does not lose the connection to its birth scope when it leaves that scope. The associated environment is kept alive by that connection and the function still has access to variable x in that environment ( x is free inside the function).

This nested way of calling add() has an advantage: if you only make the first function call, you get a version of add() whose parameter x is already filled in:

const plus2 = add(2);
assert.equal(plus2(5), 7);

Converting a function with two parameters into two nested functions with one parameter each, is called currying . add() is a curried function.

Only filling in some of the parameters of a function is called partial application (the function has not been fully applied yet). Method .bind() of functions performs partial application. In the previous example, we can see that partial application is simple if a function is curried.

4.4.0.1 Executing the code

As we are executing the following code, we are making three pauses:

function add(x) {
  return (y) => {
    // Pause 3: plus2(5)
    return x + y;
  }; // Pause 1: add(2)
}
const plus2 = add(2);
// Pause 2
assert.equal(plus2(5), 7);

This is what happens:

  • Pause 1 – during the execution of add(2) (fig. ).
  • Pause 2 – after the execution of add(2) (fig. ).
  • Pause 3 – while executing plus2(5) (fig. ).
Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 7: Closures, pause 1 – during the execution of add(2) : We can see that the function returned by add() already exists (see bottom right corner) and that it points to its birth environment via its internal property [[Scope]] . Note that plus2 is still in its temporal dead zone and uninitialized.
Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 8: Closures, pause 2 – after the execution of add(2) : plus2 now points to the function returned by add(2) . That function keeps its birth environment (the environment of add(2) ) alive via its [[Scope]] .
Deep JavaScript chapter: How do environments and closures work in JavaScript?
Figure 9: Closures, pause 3 – while executing plus2(5) : The [[Scope]] of plus2 is used to set up the outer of the new environment. That’s how the current function gets access to x .

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Visual C# 2008入门经典

Visual C# 2008入门经典

James Foxall / 张劼 / 人民邮电出版社 / 2009-6 / 39.00元

《Visual C#2008入门经典》分为五部分,共24章。第一部分介绍了Visual C# 2008速成版开发环境,引导读者熟练使用该IDE;第二部分探讨如何创建应用程序界面,包含窗体和各种控件的用法;第三部分介绍了编程技术,包括编写和调用方法、处理数值、字符串和日期、决策和循环结构、代码调试、类和对象的创建以及图形绘制等;第四部分阐述了文件和注册表的处理、数据库的使用和自动化其他应用程序等;第......一起来看看 《Visual C# 2008入门经典》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换