2

Since I'm trying to grasp the concept of iterables in JavaScript, I built one with the following code:

let range = {
  from: 0,
  to: 5
};

range[Symbol.iterator] = function() {
  return {
    current: this.from,
    last: this.to,

    next() {
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

for (let num of range) {
  console.log(num);
}

The Symbol.iterator method returns an object that contains one method (next) and two properties (current and from). The properties within this object are equal to the properties within the object range.

Since current equals this.from and last equals this.to, I assumed that I could use this.from and this.to directly from within the method next as so:

let range = {
  from: 0,
  to: 5
};

range[Symbol.iterator] = function() {
  return {
    next() {
      if (this.from <= this.to) {
        return { done: false, value: this.from++ };
      } else {
        return { done: true };
      }
    }
  };
};

for (let num of range) {
  console.log(num);
}

However, doing this prevented the iteration from starting. Having observed this, I have two questions:

  1. Why can the properties current and last use the keyword this and have it refer to the object range whereas the method next cannot? current, last, and next() are all part of the same object, which is returned by Symbol.iterator.

  2. Also, given that Symbol.iterator() returns a separate object, shouldn't the this keyword within that object refer to that object itself? In other words, shouldn't the properties current and last not be able to access the properties from range using the keyword this? Isn't range a separate object?

Gokhan
  • 187
  • 9
  • One usage of `this` is inside the `[Symbol.iterator]` method, the other in the `.next` method. Object literals don't matter, they don't have their own scope. – Bergi Aug 22 '17 at 02:49

2 Answers2

2

The keyword this is complicated. It is wise to avoid using it when there are better alternatives, which there usually are, because it has many gotchas.

With that out of the way, here's the answer: this refers to the activation object (sometimes referred to as context object) that its containing function was called with.

What is the activation object? It is the object the function was attached to when it was called. The activation object for console.log() is console.

However...

const x = {
    log : console.log
};
x.log();

... here x.log() is exactly the same as console.log(), except the activation object is x instead of console.

Seems pretty simple, right? It is, sort of. But there's more things to know.

  • There is a default context object, used when a function is called without being attached to an object. It is the global object, known as window in the browser and global in Node.js.
  • If the script is running in strict mode, then there is no default context object and it will be undefined if not explicitly called with a context
  • Arrow functions use lexical scoping and their this value is not the usual context object at all - it is the context object of its parent function from wherever the arrow function was defined

Now let's take the above and apply it to your code. Most importantly, your next() method would work if it were called with the context being the range object. The problem is, under the hood, the engine is basically doing this...

const returnedObject = range[Symbol.iterator]();
returnedObject.next();

... so next is being called with returnedObject as its context, rather than range. But the function which returned the object is called with range as its context. Thus, this is different in each place.

You can actually fix your problem very easily by using an arrow function instead of a shorthand method.

This will work:

let range = {
  from: 0,
  to: 5
};

range[Symbol.iterator] = function() {
  return {
    next : () => {
      if (this.from <= this.to) {
        return { done: false, value: this.from++ };
      } else {
        return { done: true };
      }
    }
  };
};

for (let num of range) {
  console.log(num);
}
Seth Holladay
  • 6,356
  • 2
  • 23
  • 38
1

this is known to be a tricky thing. So much so that Kyle Simpson dedicated almost a whole book to the topic (You Don't Know JS: this & Object Prototypes). However, in your example the answer is easy - in the second function, this is within the next() function and so, resolves to this function.

To check the scope of this, I am using Chrome's debugging tool. If you pause at the right line, it tells you what this resolves to.

Aydin4ik
  • 1,447
  • 1
  • 8
  • 15
  • That makes sense. However, then why do the properties `current` and `last` (both of whose values involve `this`) resolve to the `range` object and not to their own object? – Gokhan Aug 22 '17 at 02:18
  • Closure and prototypal inheritance. This says hey I am sort of this closure(function/object) – Judson Terrell Aug 22 '17 at 02:21
  • `current: this.from` happens within the `range` object and so, `this` refers to `range`. In the second snippet, `this` is used within the scope of the `next()` function. – Aydin4ik Aug 22 '17 at 02:23