09 Object Oriented Understanding of Scope and Binding Points of This Binding Through Lexicon

09 Object-Oriented Understanding of Scope and Binding Points of this Binding Through Lexicon #

Hello, I’m Ishikawa.

Today, let’s talk about this in JavaScript. There are already many resources that explain this concept clearly, but for the sake of the course’s systematic nature, I will also discuss it from the perspective of objects and object-oriented programming.

Since we’re currently in the National Day holiday, the content of this lesson is not very lengthy, so it shouldn’t be too difficult for you to learn. However, even though the text is short, the concept of this is still very important. So if you haven’t already familiarized yourself with it, I hope this lesson will help you better understand this.

At first glance, you might think that this refers to the function itself or its scope, but this understanding is incorrect. In JavaScript, this is bound at runtime rather than at writing time. So, to use it correctly, you need to consider the execution context when the function is invoked.

Default binding #

Let’s take a look at a simple example. In the example below, a is defined globally, and the aLogger function is called globally. Therefore, the this value returned is the global context, and the value of a is naturally 2.

function aLogger() {
    console.log( this.a );
}
var a = 2;
aLogger(); // 2

This default binding only works in non-strict mode. In strict mode, this default binding is not allowed, and it will return a TypeError: this is undefined.

Implicit Binding #

Next, let’s take a look at what happens when we assign the value 3 to variable a in an object obj, and then call aLogger to retrieve the value of a in the context of obj. At this point, the context of aLogger is within obj, so its value is 3.

function aLogger() {
    console.log( this.a );
}

var obj = {
    a: 3,
    logger: aLogger
};

var a = 2;

obj.logger(); // 3

However, implicit binding also has its limitations. When we assign a method from an object to a global variable, this binding is lost. For example, in the following example, when we assign obj.logger to objLogger, the this reference points to the value of a in the global scope.

function logger() {
    console.log( this.a );
}

var obj = {
    a: 3,
    logger: logger
};

var a = 2;

var objLogger = obj.logger;

objLogger(); // 2

Explicit Binding #

Now, let’s take a look at explicit binding. In this case, we use call or apply. With this method, we can forcefully set this to be equal to obj.

function logger() {
    console.log( this.a );
}

var obj = {
    a: 3
};

logger.call( obj ); // 3

However, this explicit binding cannot solve the problem completely. It can also have some side effects, such as when using new String, new Boolean, or new Number wrapped in a wrapper, this type of binding will disappear.

Hard Binding #

Next, let’s take a look at another way to achieve hard binding. Here, we use the bind method supported since ES5. By using this approach, no matter how we subsequently call the hardBinding function, the logger will always treat obj as this and retrieve the value of its a property.

function logger() {
    console.log( this.a );
}

var obj = {
    a: 3
};

var hardBinding = logger.bind( obj );

setTimeout( hardBinding, 1000 ); // 3

hardBinding.call( window ); // 3

## new binding

Finally, let's take a look at new binding. When we create a new instance using the new keyword, the new object becomes the "this" inside the function. So in the new instance, we can see that the value 2 we passed in is assigned to the property "a" of the loggerA instance, and therefore the result returned is 2.

```javascript
function logger(a) {
    this.a = a;
    console.log(this.a);
}

var loggerA = new logger(2); // 2

Now let’s have a “showdown” between hard binding and new binding to see who has the absolute power. First, we hard bind the “this” inside the logger function to obj1, and the result we get is 2. Then, we create a new logger instance using the new keyword. In this instance, we can see that obj2 is a new logger instance, and its “this” is not influenced by obj1. Therefore, new binding is more powerful than hard binding.

function logger(a) {
    this.a = a;
}

var obj1 = {};

var hardBinding = logger.bind(obj1);

hardBinding(2);

console.log(obj1.a); // 2

var obj2 = new logger(3);

console.log(obj1.a); // 2
console.log(obj2.a); // 3

Previously, someone mentioned MDN, which is a great platform to learn about JavaScript and is run by Mozilla, the company behind the Firefox browser. By looking at the code of the bind polyfill on MDN, we can see that there is a logical check inside the bind function. It checks if the new instance is created using the new keyword, and if it is, then the “this” is bound to the new instance.

this instanceof fNOP &&
oThis ? this : oThis

// ... and:

fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();

So what are the advantages of using new and bind? Using new allows us to ignore hard binding and also allows us to preset function arguments. Using bind allows any arguments following the “this” to be treated as default arguments. This can be used to create partial applications, as we discussed in lesson 3. In the example below, 1 and 2 are the default arguments, and when we provide 9 to partialFunc, we get the result of adding three numbers.

function fullFunc(x, y, z) {
  return x + y + z;
}

const partialFunc = fullFunc.bind(this, 1, 2);
partialFunc(9); // 12

In addition to hard binding, there is also a soft binding method, which binds “this” to a default object in the case of a global or undefined “this”.

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this,
            curried = [].slice.call(arguments, 1),
            bound = function bound() {
                return fn.apply(
                    (!this ||
                        (typeof window !== "undefined" &&
                            this === window) ||
                        (typeof global !== "undefined" &&
                            this === global)
                    ) ? obj : this,
                    curried.concat.apply(curried, arguments)
                );
            };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    };
}

In the example below, we can see that obj2 returns the default binding result in the global scope of the timeout function, in addition to the implicit, explicit, and soft bindings.

function logger() {
   console.log("name: " + this.name);
}
var obj1 = { name: "obj1" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

var logger1 = logger.softBind(obj1);
logger1(); // name: obj1

obj2.logger = logger.softBind(obj1);
obj2.logger(); // name: obj2   

logger1.call(obj3); // name: obj3   

setTimeout(obj2.logger, 1000); // name: obj1

Similarly, this soft binding also supports partial application in currying, as we discussed earlier.

function fullFunc(x, y, z) {
  return x + y + z;
}

const partialFunc = fullFunc.softBind(this, 1, 2);
partialFunc(9); // 12

Extension: Arrow Functions #

When it comes to the binding of this, there is one thing we need to pay attention to: when using arrow functions, this is bound lexically, not based on the context of function execution. For example, in the following example, the returned result is 2 instead of 3.

function logger() {
    return (a) => {
        console.log(this.a);
    };
}
var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
};

var logger1 = logger.call(obj1);

logger1.call(obj2); // 2

A common scenario for using arrow functions to bind this is setTimeout. In this function, this will be bound in the lexical scope of the logger function.

function logger() {
    setTimeout(() => {
        console.log(this.a);
    }, 1000);
}
var obj = {
    a: 2
};
logger.call(obj); // 2

If we don’t use arrow functions, we can also bind this in the lexical scope with self = this.

function logger() {
    var self = this;
    setTimeout(function(){
        console.log(self.a);
    }, 1000);
}
var obj = {
    a: 2
};
logger.call(obj); // 2

However, for the sake of code readability and maintainability, it is usually better to use lexical scope consistently in the same function, either by avoiding the use of this altogether, or by using methods like bind to bind this, rather than using arrow functions or “tricks” like self = this.

Summary #

In this class, we learned about the binding of this, which can be considered as a concept of equal importance to closures in functional programming. If understanding closures is essential to functional programming, then not understanding this would leave one confused when using object-oriented programming in JavaScript as well. Although understanding these two concepts can be a bit confusing at first, once you comprehend them, you will realize that they are ubiquitous.

Thought-provoking Question #

Today, when we talked about the binding of this, we used call and bind. We know that in JavaScript, there is also apply, which is similar to call. Do you think apply has the same effect as call when it comes to binding?

Feel free to share your answers, discuss your learning experiences, or ask questions in the comments section. If you find it helpful, you are also welcome to share today’s content with more friends.