How JavaScript Works. Why understanding the Fundamentals is… | by Ionel Hindorean | Better Programming

Scope and scope chains

Scope contains variables and functions that are accessible within a particular function, even if they are not declared in the function itself.

JavaScript has lexical scope, which means that the scope is determined by where a function is declared in the code.

function a() {
  function b() {
    console.log(foo); // logs 'bar'
  }

  var foo = 'bar';
  b();
}

a();
Copy the code

Upon reaching console.log(foo) above, the JavaScript engine first checks to see if there is a variable foo in the scope of b() ‘s execution context.

Since there is no declaration, it goes to the “parent” level execution context, which is the execution context of a(), since b() is declared in a(). Within the scope of this execution context, it finds foo and prints out its value.

If we extract b() from a(), like this:

function a() {
  var foo = 'bar';
  b();
}

function b() {
  console.log(foo); // throws ReferenceError: foo is not defined
}

a();
Copy the code

A ReferenceError will be thrown, although the only difference between the two is where b() is declared.

The “parent” scope of b() is now the scope of the global execution context, because it is declared at the global level, outside of any function, and there is no variable foo there.

I can see why this might be confusing, because if you look at the execution stack, it looks like this:

Therefore, it is easy to think that the “parent” execution environment is next in the stack, below the current environment. However, this is not true.

In the first example, the execution context of a() is indeed the “parent” execution context of b(). This is not because a() happens to be the next item in the execution stack, just below b(), but simply because b() is declared in a().

In the second example, the execution stack looks the same, but this time the “parent” execution context of b() is the global execution context, since b() is declared at the global level.

Remember: it doesn’t matter where a function is called, it matters where it is declared.

But what happens if it also can’t find the variable in the scope of the “parent” level execution context?

In this case, it will try to find it in the scope of the next “parent” level execution context, which is determined in exactly the same way.

If it isn’t there either, it will try the next one, and so on, until finally, it reaches the scope of the global execution context. If it can’t find it there either, it will throw a ReferenceError.

This is called scoping, and that’s exactly what happens in the following example:

function a() {
  function b() {
    function c() {
      console.log(foo);
    }
    
    c();
  }
  
  var foo = 'bar';
  b();
}

a();
Copy the code

It first tries to find foo within the scope of c() ‘s execution context, then b(), and finally a(), where it finds it.

Note: Remember, it goes from C () to b() to a() only because they are declared in each other, not because their corresponding execution context is above each other on the execution stack.

If they are not declared inside each other, the execution context of the “parent” level will be different, as described above.

However, if there is another variable foo in c() or b(), its value will be recorded to the console, because once the engine finds this variable, it will stop “looking for” the “parent” execution context.

This also applies to functions, not just variables, and also to global variables, such as console itself, above.

It will go down the scope chain (or up, depending on how you look at it) looking for a variable named console, which it will eventually find in the global execution context, attached to the WinODW object.

Note: Although I only used function declaration syntax in the above example, scopes and scope chains do exactly the same for arrow functions, which were introduced in ES2015 (also known as ES6).

closure

Closures provide scope for accessing external functions from internal functions.

However, this is nothing new, and I have described above how it is achieved through scoped chains.

Closures are special in that the inner function still has a reference to the scope of the outer function, even if the outer function’s code is executed and its execution context is popped off the execution stack and destroyed.

function a() {
  var name = 'John Doe';
  
  function b() {
    return name;
  }

  return b;
}

var c = a();

c();
Copy the code

B () is declared in a(), so it can access the name variable from the scope of a() through the scope chain.

But not only does it access it, it also creates a closure, which means it can access it even after the parent function a() returns.

The variable c is a reference to the internal function b(), so the last line of code actually calls the internal function b().

Although this happens long after the outer function a() of b() has returned, the inner function b() can still access the scope of the parent function.

You can read more about using closures on Medium by Eric Elliott.

this

The next thing to determine in the execution context creation step is the value of this.

I’m afraid this is not as simple as scope, because the value of this in a function depends on how the function was called. And, to make it even more complicated, you can override the default behavior.

I’ll try to keep the explanation simple and clear, but you can find a more detailed article on this topic at MDN.

First, it depends on whether the function uses a function declaration:

function a() {
  // ...
};
Copy the code

Or an arrow function:

const a = () = > {
  // ...
};
Copy the code

As mentioned above, both scopes are identical, but this is different.

Arrow function

I’ll start with the simplest one. In the case of the arrow function, this is static, so it is determined in a similar way to the scope.

The “parent” level execution context is determined exactly as the scope and scope chain sections are interpreted, depending on where the arrow function is declared.

This will be the same as this value in the parent execution context, where this value is determined as described in this section.

We can see this in the following two examples.

The first one is recorded as true and the second as false, although in both cases myArrowFunction is called from the same place. The only difference between the two is where the arrow function myArrowFunction is declared:

const myArrowFunction = () = > {
  console.log(this= = =window);
};

class MyClass {
  constructor(){ myArrowFunction(); }}var myClassInstance = new MyClass();
Copy the code
class MyClass {
  constructor() {
    const myArrowFunction = () = > {
      console.log(this= = =window); }; myArrowFunction(); }}var myClassInstance = new MyClass();
Copy the code

Since this is static in myArrowFunction, it will be the window in the first example because it is declared at the global level, outside of any function or class.

In the second example, this in myArrowFunction is any value of this in the function that wraps it.

I’ll discuss exactly what that value is later in this section, but for now just note that it’s not a window, as in the first example.

Please remember that. For arrow functions, the value of this is determined by where the arrow function is declared, not by where or how it is called.

Function declaration

In this case, it’s not so simple, which is why the arrow function (or at least one of them) was introduced in ES2015, but bear with me, the next few paragraphs make sense.

Except for the arrow function (const a = () => {… }) and (function a() {… In addition to the grammatical differences between the two, the internal this is the main difference.

Unlike arrow functions, the value of this in a function declaration is not determined lexically based on where the function is declared.

It depends on how the function is called. There are several ways you can call a function: ‘

  • Simple call:myFunction()
  • Object method calls:myObject.myFunction()
  • Constructor call:new myFunction()
  • DOM event handler call:document.addEventListener('click', myFunction)

The value in myFunction() is different for each type of this, regardless of where myFunction() is declared, so let’s take a look at it one by one and see how it works.

The statement calls

function myFunction() {
  return this= = =window; // true
}

myFunction();
Copy the code

A simple call is a simple call to a function, as in the example above. A single function name, without any preceding characters, followed by () (with any optional arguments, of course).

In the case of a simple call, this in the function is always global this, which in turn points to the Window object, as described in the previous section.

So be it! But remember, this is only for simple calls; The function name is followed by (). There is no preceding character.

Note: Because this is actually a reference to window in a simple function call, it is considered bad practice to use this in functions that are intended to be called simply.

This is because any property that is attached to this in a function is actually attached to window and becomes a global variable, which is bad practice.

This is why in strict mode, the value of this is undefined in any function that is simply called, and the above example prints false.

Object method call

const myObject = {
  myMethod: function() {
    return this === myObject; // true}}; myObject.myMethod();Copy the code

When a property of an object has a function as its value, it is considered a method of the object and is therefore called a method call.

When this type of call is used, the this inside the function simply points to the object on which the method was called, which is myObject in the example above.

Note: If you use the arrow function syntax instead of the function declaration in the example above, the value of this in the arrow function will be the window object.

This is because its parent execution environment is the global execution environment. The fact that it is declared in an object does not change.

Constructor call

Another way to call a function is to use the new keyword before the call, as shown in the following example.

When called this way, the function returns a new object (even if it does not return a statement), and the this inside the function points to that newly created object.

This interpretation is a bit simplified (there is more interpretation on MDN), but the point is that it will create (or construct, hence the constructor) and return an object to which this will refer in the function.

function MyConstructorFunction() {
  this.a = 1;
}

const myObject = new MyConstructorFunction(); // a new object is created

// inside MyConstructorFunction(), "this" points to the newly created onject,
// so it should have a property "a".
myObject.a; / / 1
Copy the code

Note: This is also true when using the new keyword on a class, because classes are really special functions with very small differences.

Note: Arrow functions cannot be used as constructors.

DOM event function calls

document.addEventListener('click', DOMElementHandler);

function DOMElementHandler() {
  console.log(this= = =document); // true
}
Copy the code

When called as a DOM event handler, the value inside the function will be the DOM element in which the event is located.

Note: In all other types of calls, we control function calls ourselves.

However, in the case of event handlers, we don’t do this; we just pass a reference to the handler function. The javascript engine will call this function, and we have no control over how it is called.

The customthisThe call

This in a Function can be explicitly set to a custom value by calling it using bind(), call(), or apply() in function.prototype.

const obj = {};

function a(param1, param2) {
	return [this= = =window.this === obj, param1, param2];
}

a.call(obj, 1.2); // [false, true, 1, 2]
a.apply(obj, [3.4]); // [false, true, 3, 4]
a.bind(obj)(5.6);  // [false, true, 5, 6]
a(7.8);   // [true, false, 7, 8]
Copy the code

The examples above show how each of them works.

Call () and apply() are very similar; the only difference is that in apply(), the arguments to the function are passed as arrays.

Call () and apply() actually call functions with the value set to the first argument you pass, while bind() does not call functions.

Instead, it returns a new function that is exactly the same as the function used by bind(), except that it sets the value of this to what you passed to bind() as an argument.

That’s why you see (5, 6) after a.bind(obj), which is actually calling the function returned by bind().

In the case of bind(), the value of this in the returned function is permanently bound to the value of this you passed (thus called bind()).

No matter which type of call is used, the value in the returned function is always the one provided as an argument. It can only be modified again by call(), bind(), or apply().

The above statement is almost exactly right. Of course, there must be one exception to this rule, and that exception is the constructor call.

When a function is called in this way, by placing the new keyword before its call, the value of this within the function will always be the object returned by the call, even if the new function gives another this with bind().

You can check this in the following example:

function a() {
  this.three = 'three';
  console.log(this);
}
​
const customThisOne = { one: 'one' };
const customThisTwo = { two: 'two' };
​
const bound = a.bind(customThisOne); // returns a new function with the value of this bound to customThisOne
bound(); // logs customThisOne
bound.call(customThisTwo); // logs customThisOne, even though customThisTwo was passed to .call()
bound.apply(customThisTwo); // same as above
new bound(); // logs the object returned by the new invocation, bypassing the .bind(customThisOne)
Copy the code

Here’s an example of how you can use bind() to control the value of the click event handler we discussed earlier:

const myCustomThis = {};
document.addEventListener('click', DOMElementHandler.bind(myCustomThis));

function DOMElementHandler() {
  console.log(this= = =document); // false (used to be true before bind() was used)
  console.log(this === myCustomThis); // true
}
Copy the code

Note: Bind (), call(), and apply() cannot be used to pass a custom this to the arrow function.

A description of the arrow function

You can now see that the rules for declaring these functions, although fairly simple, can cause confusion and can be a source of errors due to some special cases.

A small change in how a function is called will change the value in it. This can lead to a whole chain reaction, which is why it’s important to understand these rules and how they affect your code.

That’s why the people who wrote the JavaScript specification came up with arrow functions, where this is always static and is exactly the same every time, regardless of how they were called.

ascension

As I mentioned earlier, when a function is called, the JavaScript engine first browses the code, looking for scope and this, and determining which variables and functions are declared in the function body.

In this first step (the creation step), these variables get a special value undefined, regardless of the actual value assigned to them in the code.

It is only in the second step (the execution step) that they are assigned to the actual value, and this only happens when the assignment line is reached.

This is why the following JavaScript code is recorded as undefined:

console.log(a); // undefined

var a = 1;
Copy the code

In the creation step, the variable A is identified and given the special value undefined.

Then, in the execution step, you reach the line that records A to the console. Undefined is recorded because this is the value set to a in the previous step.

When you reach the line where A is assigned to 1, the value of A will change to 1, but undefined has already been logged to the console.

This effect is called promotion, as if all variable declarations were promoted to the top of the code. As you can see, that’s not really what’s happening, but that’s the term used to describe it.

Note: This also happens with arrow functions, not just function declarations.

In the create step, the function is not given a special value, undefined, but the entire body of the function is put into memory. This is why a function can be called even before it is declared, as in the following example, and it still works:

a();

function a() {
  alert("It's me!");
}
Copy the code

Note: A ReferenceError: x is not defined is thrown when attempting to access a variable that is not defined at all. So, there’s a difference between “undefined” and “not defined”, which can be a little confusing.

conclusion

I remember reading articles about promotion, scopes, closures, etc., which made sense when I read them, but then I kept coming across some weird JavaScript behavior that I just couldn’t explain.

The problem is, I always read each concept individually, one at a time.

So I try to understand the big things, like the JavaScript engine itself. How the execution context is created and pushed onto the execution stack, how the event queue works, how this and scope are determined, and so on.

After that, everything else makes sense. I started identifying potential problems earlier, identifying the source of errors faster, and became more confident in my coding overall.

I hope this article will do the same for you!

reference

  • Programming JavaScript Applications
  • JavaScript: Understanding the Weird Parts (first three and a half hours for free here)
  • You don ‘t know JS