Can't destructure an object inside a function generator

Viewed 241

I want to destructure the result of a previous yield using default values when the object is empty. But I'm getting a Cannot read property 'xxx' of undefined, meaning that where I try to destructure the variable theObject is undefined, but why?

const DEFAULT_POSITION = {x: 20, y: 20}
const myObject = {}

function* myGenerator(i) {
  const theObject = yield myObject;
  const { posX = DEFAULT_POSITION.x, posY = DEFAULT_POSITION.y, scale = 1 } = theObject

  yield {posX, posY, scale}
}

The first yield returns me an empty object as expected, but then when I run the generator again I get the error that the first item (posX) in the object destruction can not be read since the theObject is undefined.

2 Answers

The problem is that you yield myObject to the caller, the result of that yield is read from the caller via next(/* this "undefined" argument gets passed into your generator function*/) call. So theObject is undefined because you're not using the generator as intended.

When the iterator's next() method is called, the generator function's body is executed until the first yield expression, which specifies the value to be returned from the iterator

Calling the next() method with an argument will resume the generator function execution, replacing the yield expression where execution was paused with the argument from next()

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*

It's not very intuitive, generator functions are weird like that.

The following works but is probably not what you want as far as I understand

const DEFAULT_POSITION = {x: 20, y: 20}
const myObject = {}

function* myGenerator(i) {
  const theObject = yield
  const { posX = DEFAULT_POSITION.x, posY = DEFAULT_POSITION.y, scale = 1 } = theObject

  yield {posX, posY, scale}
}
const it = myGenerator()
console.log(it.next()) // -> {value: undefined, done: false}
console.log(it.next(myObject)) // -> {value: { posX: 20, posY: 20, scale: 1 }, done: false}

Understanding the Generator

One way of thinking of yield is as a breakpoint, which returns a value to the calling body. It can also pass a value in the generator body, but the value needs to be passed in using next() as described in MDN: Generator#next:

in variable = yield expression, the value passed to the .next() function will be assigned to variable.

In your case, there is no value passed to next() (at least not in your given code), so theObject is undefined and encountering an error when used in the restructuring assignment.

I've attempted to rework your code in two examples, w/ and w/o an argument passed to it for initialization and then some looping to demonstrate using a previous value.


No Arguments Passed to Generator

The following passes no arguments to the generator function. The multiple yields are intended to demonstrate the returned value from the Generator.

function* GetPositions(args={}) {
  const defaults = {x:20,y:20}
  const position = Object.assign({}, defaults, args)
  yield position             // return initial setup
  
  const { x:posX, y:posY, scale = 1 } = position
  yield {posX, posY, scale}  // return next value
}


let coord = GetPositions()
console.log( coord.next().value )  // {x:20, y:20}
console.log( coord.next().value )  // {posX:20, posY:20, scale:1}


Passing Arguments to Generator

The following passes one object to the generator function. It is only a partial object as it contains one key to demonstrate using internal defaults.

function* GetPositions(args={}) {
  const defaults = {x:20,y:20}
  const position = Object.assign({}, defaults, args)
  yield position             // return initial setup
  
  const { x:posX, y:posY, scale = 1 } = position
  yield {posX, posY, scale}  // return next value
}

let coord = GetPositions({x:1})   // initial argument
console.log( coord.next().value ) // {x:1, y:20}
console.log( coord.next().value ) // {posX:1, posY:20, scale:1}


Looping in Generator

This would be closer to what you might use a generator for, since it is more common to use a generator in a repeated fashion, to generate sequences or advance values.

function* GetPositions(args={}) {
  const defaults = {x:20,y:20}
  const position = Object.assign({}, defaults, args)
  let { x:posX, y:posY, scale = 1 } = position
  
  yield {posX, posY, scale}
  
  while(true){
    posX+=5, posY+=5
    yield {posX, posY, scale}     // return next value
  }
}

let coord = GetPositions({x:1})   // initial argument

console.log( coord.next().value ) // {posX:1,  posY:20, scale:1}
console.log( coord.next().value ) // {posX:6,  posY:25, scale:1}
console.log( coord.next().value ) // {posX:11, posY:30, scale:1}

Related