50

What is `this`? The Inner Workings of JavaScript Objects

 5 years ago
source link: https://www.tuicool.com/articles/hit/AFVR3iB
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.

JavaScript is a multi-paradigm language that supports object-oriented programming and dynamic binding. Dynamic binding is a powerful concept which allows the structure of your JavaScript code to change at runtime, but that extra power and flexibility comes at the cost of some confusion, and a lot of that confusion is centered around how this behaves in JavaScript.

Dynamic Binding

Dynamic binding is the process of determining the method to invoke at runtime rather than compile time. JavaScript accomplishes that with this and the prototype chain. In particular, the meaning of this inside a method is determined at runtime, and the rules change depending on how that method was defined.

Let’s play a game. I call this game “What is this ?"

const a = {
  a: 'a'
};
const obj = {
  getThis: () => this,
  getThis2 () {
    return this;
  }
};
obj.getThis3 = obj.getThis.bind(obj);
obj.getThis4 = obj.getThis2.bind(obj);
const answers = [
  obj.getThis(),
  obj.getThis.call(a),
  obj.getThis2(),
  obj.getThis2.call(a),
  obj.getThis3(),
  obj.getThis3.call(a),
  obj.getThis4(),
  obj.getThis4.call(a)
];

Before you continue, write down your answers. After you’ve done so, console.log() your answers to check them. Did you guess right?

Let’s start with the first case and work our way down. obj.getThis() returns undefined , but why? Arrow functions can never have their own this bound. Instead, they always delegate to the lexical scope. In the root scope of an ES6 module, the lexical scope in this case would have an undefined this . obj.getThis.call(a) is also undefined, for the same reason. For arrow functions, this can't be reassigned, even with  .call() or  .bind() . It will always delegate to the lexical this .

obj.getThis2() gets its binding via the normal method invocation process. If there is no previous this binding, and the function can have this bound (i.e., it's not an arrow function), this gets bound to the object the method is invoked on with the  . or [squareBracket] property access syntax.

obj.getThis2.call(a) is a little trickier. The call() method calls a function with a given this value and optional arguments. In other words, it gets its this binding from the  .call() parameter, so obj.getThis2.call(a) returns the a object.

With obj.getThis3 = obj.getThis.bind(obj); , we're trying to bind an arrow function, which we've already determined will not work, so we're back to undefined for both obj.getThis3() , and obj.getThis3.call(a) .

You can bind regular methods, so obj.getThis4() returns obj , as expected, and because it's already been bound with obj.getThis4 = obj.getThis2.bind(obj); , obj.getThis4.call(a) respects the first binding and returns obj instead of a .

Curve Ball

Same challenge, but this time, with class using the public fields syntax ( Stage 3 at the time of this writing, available by default in Chrome and with @babel/plugin-proposal-class-properties ):

class Obj {
  getThis = () => this
  getThis2 () {
    return this;
  }
}
const obj2 = new Obj();
obj2.getThis3 = obj2.getThis.bind(obj2);
obj2.getThis4 = obj2.getThis2.bind(obj2);
const answers2 = [
  obj2.getThis(),
  obj2.getThis.call(a),
  obj2.getThis2(),
  obj2.getThis2.call(a),
  obj2.getThis3(),
  obj2.getThis3.call(a),
  obj2.getThis4(),
  obj2.getThis4.call(a)
];

Write your answers down before you continue.

Ready?

With the exception of obj2.getThis2.call(a) , these all return the object instance. The exception returns the a object. The arrow function still delegates to lexical this . The difference is that lexical this is different for class properties. Under the hood, that class property assignment is being compiled to something like this:

class Obj {
  constructor() {
    this.getThis = () => this;
  }
...

In other words, the arrow function is being defined inside the context of the constructor function. Since it’s a class, the only way to create an instance is to use the new keyword (omitting new will throw an error).

One of the most important things that the new keyword does is instantiate a new object instance and bind this to it in the constructor. This behavior, combined with the other behaviors we've already mentioned above should explain the rest.

Conclusion

How did you do? Did you get them all right? A good understanding of how this behaves in JavaScript will save you a lot of time debugging tricky issues. If you got any of the answers wrong, it would serve you well to practice. Play with the examples, then come back and test yourself again until you can both ace the test, and explain to somebody else why the methods return what they return.

If that was harder than you expected, you’re not alone. I’ve tested quite a few developers on this topic, and I think only one developer has aced it so far.

What started as dynamic method lookups that you could redirect with .call().bind() , or  .apply() has become significantly more complex with the addition of class and arrow function behavior. It may be helpful to compartmentalize a little. Remember that arrow functions always delegate this to the lexical scope, and that class this is actually lexically scoped to the constructor functions under the hood. If you're ever in doubt about what this is, remember to use your debugger to verify the object is what you think it is.

Remember also that in JavaScript, you can do a lot without ever using this . In my experience, almost anything can be reimplemented in terms of pure functions which take all the arguments they apply to as explicit parameters (you can think of this as an implicit parameter with mutable state). Logic encapsulated in pure functions is deterministic, which makes it more testable, and has no side-effects, which means that unlike manipulating this , you're unlikely to break anything else. Every time you mutate this , you take the chance that something else dependent on the value of this will break.

That said, this is sometimes useful. For instance, to share methods between a large number of objects. Even in functional programming, this can be useful to access other methods on the object to implement algebraic derivations to build new algebras on top of existing ones. For instance, a generic  .flatMap() can be derived by accessing this.map() and this.constructor.of() .


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK