10

Unpacking hoisting

 4 years ago
source link: https://www.tuicool.com/articles/amqMzee
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Quoting a recent tweet by ES6 spec author Allen Wirfs-Brock:

Hoisting is old and confused terminology. Even prior to ES6: did it mean “moved to the top of the current scope” or did it mean “move from a nested block to the closest enclosing function/script scope”? Or both?

This blog post proposes a different approach to describing declarations (inspired by a suggestion by Allen).

Declarations: scope and activation

I propose to distinguish two aspects of declarations:

  • Scope: Where can a declared entity be seen? This is a static trait.
  • Activation: When can I access an entity? This is a dynamic trait: Some entities can be accessed as soon as we enter their scopes. For others, we have to wait until execution reaches their declarations.

The following table summarizes how various declarations handle these aspects. “Duplicates” describes whether or not it is allowed to declare a name twice within the same scope. “Global prop.” describes if a declaration adds a property to the global object when it is executed in a script (a precursor to modules), in global scope. TDZ means temporal dead zone (which is explained later).

Scope Activation Duplicates Global prop. const Block decl. (TDZ) let Block decl. (TDZ) function Block start class Block decl. (TDZ) import Module same as export var Function start, partially

The following sections describe the behavior of some of these constructs in more detail.

const and let : temporal dead zone  

For JavaScript, TC39 needed to decide what happens if you access a constant in its direct scope, before its declaration:

{
  console.log(x); // What happens here?
  const x;
}

Some possible approaches are:

undefined

(1) was rejected, because there is no precedent in the language for this approach. It would therefore be not intuitive to JavaScript programmers.

(2) was rejected, because then x wouldn’t be a constant – it would have different values before and after its declaration.

let uses the same approach (3) as const , so that both work similarly and it’s easy to switch between them.

The time between entering the scope of a variable and executing its declaration is called the temporal dead zone (TDZ) of that variable:

ReferenceError
undefined

The following code illustrates the temporal dead zone:

if (true) { // entering scope of `tmp`, TDZ starts
  // `tmp` is uninitialized:
  assert.throws(() => (tmp = 'abc'), ReferenceError);
  assert.throws(() => console.log(tmp), ReferenceError);

  let tmp; // TDZ ends
  assert.equal(tmp, undefined);
}

The next example shows that the temporal dead zone is truly temporal (related to time):

if (true) { // entering scope of `myVar`, TDZ starts
  const func = () => {
    console.log(myVar); // executed later
  };

  // We are within the TDZ:
  // Accessing `myVar` causes `ReferenceError`

  let myVar = 3; // TDZ ends
  func(); // OK, called outside TDZ
}

Even though func() is located before the declaration of myVar and uses that variable, we can call func() . But we have to wait until the temporal dead zone of myVar is over.

Function declarations and early activation

A function declaration is always executed when entering its scope, regardless of where it is located within the scope. That enables you to call a function foo() before it is declared:

assert.equal(foo(), 123); // OK
function foo() { return 123; }

The early activation of foo() means that the previous code is equivalent to:

function foo() { return 123; }
assert.equal(foo(), 123);

If you declare a function via const or let , then it is not activated early: In the following example, you can only use bar() after its declaration.

assert.throws(
  () => bar(), // before declaration
  ReferenceError);

const bar = () => { return 123; };

assert.equal(bar(), 123); // after declaration 

Class declarations are not activated early, either:

assert.throws(
  () => new MyClass(),
  ReferenceError);

class MyClass {}

assert.equal(new MyClass() instanceof MyClass, true);

Calling ahead without early activation

Even if a function g() is not activated early, it can be called by a preceding function f() (in the same scope) – if we adhere to the following rule: f() must be invoked after the declaration of g() .

const f = () => g();
const g = () => 123;

// We call f() after g() was declared:
assert.equal(f(), 123);

The functions of a module are usually invoked after its complete body was executed. Therefore, in modules, you rarely need to worry about the order of functions.

Lastly, note how early activation automatically keeps the aforementioned rule: When entering a scope, all function declarations are executed first, before any calls are made.

A pitfall of early activation

If you rely on early activation to call a function before its declaration, then you need to be careful that it doesn’t access data that isn’t activated early.

funcDecl();

const MY_STR = 'abc';
function funcDecl() {
  assert.throws(
    () => MY_STR,
    ReferenceError);
}

The problem goes away if you make the call to funcDecl() after the declaration of MY_STR .

The pros and cons of early activation

We have seen that early activation has a pitfall and that you can get most of its benefits without using it. Therefore, it is better to avoid early activation. But I don’t feel strongly about this and, as mentioned before, often use function declarations, because I like their syntax.

Class declarations are not activated early

Consider the following class declaration:

class MyClass extends Object {}

extends is optional. Its operand is an expression. Therefore, you can do things like this:

const identity = x => x;
class MyClass extends identity(Object) {}

Evaluating such an expression must be done at the location where it is mentioned. Anything else would be confusing. That explains why class declarations are not activated early.

var : hoisting (partial early activation)  

var is an older way of declaring variables that precedes const and let (which are preferred now). Consider the following var declaration.

var x = 123;

This declaration has two parts:

  • Declaration var x : The scope of a var -declared variable is the innermost surrounding function and not the innermost surrounding block, as for most other declarations. Such a variable is already active at the beginning of its scope and initialized with undefined .
  • Assignment x = 123 : The assignment is always executed in place.

The following code demonstrates var :

function f() {
  // Partial early activation:
  assert.equal(x, undefined);
  if (true) {
    var x = 123;
    // The assignment is executed in place:
    assert.equal(x, 123);
  }
  // Scope is function, not block:
  assert.equal(x, 123);
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK