1

I'm trying to use JS with classic prototypical inheritance, instead of the new ES6 class model, mainly to be able to access the closure scope.

In the example bellow, I want to expose a variable current declared inside the function Counter trough this object created by the new operator.

function Counter(start, stop) {
  var current = start;

  function inc() { if (current < stop) return current++ }

  function getCurrent() { return current }

  Object.assign(this, { inc, getCurrent,
    get current() { return current }, set current(value) { current = value }
  })
}

counter = new Counter(0, 3)

while ((v = counter.inc()) !== undefined)
   console.log(counter.getCurrent(), counter.current)

I expected the following output:

1 1 
2 2
3 3

Because counter.current & counter.getCurrent() should both return the same result. But instead, I'm receiving

1 0 
2 0
3 0

If I replace that Object.assign(...) with the code bellow, it works as expected.

  Object.assign(inc, getCurrent })
  Object.defineProperty(Counter.prototype, 'current',
    { get: () => { return current }, set: (value) => { current = value } 

I could use this model, (and currently using), but I would like to use the former, because is simpler and less verbose. It seems that there are 2 different scopes here.

I tested with node 10, Chrome 73 & Firefox 68, and received the same results.

What I'm missing here?


In the example above, I tried to be as terser as I could. But to be more precise and better illustrate the point, follow a more complete test, with some commented things I've tried.

Here, I renamed the variable current to _current in order to avoid confusion with the current property, but that shouldn't be obligatory.


function Counter(start, stop) {
  var _current = start;
  function inc() { if (_current < stop) return _current++ }
  function getCurrent() { return _current }

  // Object.assign(this.constructor.prototype,
  // Object.assign(this.__proto__,
  // Object.assign(Counter.prototype, {

  Object.assign(this, {inc, getCurrent,
    get current() { return _current }, set current(value) { _current = value }
    // get current() { return current } // supposed to be read-only, but not
  })

  // This works as expected
  // Object.defineProperty(Counter.prototype, 'current',
  //   { get: () => { return _current }, set: (value) => { _current = value } })
}

counter = new Counter(0, 3)

while ((v = counter.inc()) !== undefined) {
  console.log(counter.getCurrent(), counter.current)
  counter.current -= 0.5
}

the output of the code above is:

1 0
2 -0.5
3 -1

Where that counter.current -= 0.5 is storing its value?

bittnkr
  • 99
  • 10
  • 2
    `Object.assign` doesn't copy getters, it evaluates them. Just `return` your object literal from `Counter`. Or do the `Object.defineProperty` call on `this`. – Bergi Sep 07 '19 at 23:34
  • I cannot return the object literal, because that way, I'll loose the type Counter() of this. And I believe that previous getter/setter question doesn't have the answer to my question. Why the outputs are different? Where the second attribution ```counter.current -= 0.5``` is storing the value? – bittnkr Sep 08 '19 at 00:14
  • What do you mean by "type of this"? You didn't intend to use the prototype anyway. – Bergi Sep 08 '19 at 00:53
  • 1
    Regarding your edited question, `counter.current` is a simple data property. The `Object.assign(…)` call you made had exactly the same result as `this.inc = inc; this.getCurrent = getCurrent; this.current = _current; // 0`. The `-= 0.5` assignment simple stores the new number right on the `counter` object. `inc` and `getCurrent` are still closures over the internal `_current` variable. – Bergi Sep 08 '19 at 00:58
  • When you said: "Just return your object literal " I understood something like ```return {inc, getCurrent, current}``` It's what you are thinking? If I do that, the returned value will not be of Counter() type of this, but just an ordinary Object. – bittnkr Sep 08 '19 at 01:19
  • 1
    I mean the one with the `get current() { … }, set current(…) { … }`, but yes. Sure that's an ordinary object, but it doesn't matter, it has all the functionality you wanted. Why do you care about the type of the object, what do you need it for? – Bergi Sep 08 '19 at 01:21
  • Yes, you are right, if I return the object literal with the get/set (without Object.assign) it will works as expected. To be honest, just I like to see the types of the object, I believe its an habit gained with 20 years using C++ and other typed languages. :( – bittnkr Sep 08 '19 at 01:34
  • If it's just about "seeing types" in the console, [you can customise that](https://nodejs.org/api/util.html#util_custom_inspection_functions_on_objects) :-) – Bergi Sep 08 '19 at 01:37

1 Answers1

1

When your code calls Object.assign(), it's passing in an object that has a getter and setter for current. Thus the Object.assign() process will itself invoke that getter to get the value for the property "current" while it's copying the property values to the new object. Thus, the Counter object ends up without the getter and setter, which explains your results. Its "counter" property is just a simple property with a copy of the value of the local counter variable at the time the constructor code ran.

Object.assign() just copies property values, accessing them the same way any other code would.

Note that if you don't call Object.assign() at all, and just return the object you're passing into it, you'll get a working object that behaves like you expect.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • I believe that on Object.assign() there is no current property yet, but just a current *variable*, I understand that I'm mixing two objects, and that the get/set is owned by the second, but I still didn't get the point. Can you point another way to get the desired result? – bittnkr Sep 07 '19 at 23:27
  • 1
    @bittnkr consider what `Object.assign()` does: it internally will do something like `Object.keys()` to get the list of *names* of the properties of the source object, and in your case "current" is one of those names. When code asks for the value of the property called "current", what happens? That's right, the getter in your *source* object is called, and that returns 0. `Object.assign()` does not care that the property value came from a getter function. It just makes a property on the *target* object and gives it the value 0. – Pointy Sep 07 '19 at 23:31
  • 1
    OK. I understood that Object.assign() copy the value of the property on the first ```Object.assign()``` call, and **do not** copy the getter/setter. Afterwards all access to the ```current``` property is on this object as a ordinary property, and don't touch the getter or setter anymore. It seems that ```Object.defineProperty()``` is the only way to do what I want. – bittnkr Sep 08 '19 at 01:14